Spaces:
Paused
Paused
Upload 16 files
Browse files- .gitattributes +3 -0
- Dockerfile +1 -1
- README.md +41 -10
- index.html +1406 -0
- main.py +877 -0
- requirements.txt +1 -0
- static/.DS_Store +0 -0
- static/css/buefy.min.css +0 -0
- static/css/materialdesignicons.css.map +0 -0
- static/css/materialdesignicons.min.css +0 -0
- static/favicon.ico +0 -0
- static/fonts/materialdesignicons-webfont.ttf +3 -0
- static/fonts/materialdesignicons-webfont.woff +3 -0
- static/fonts/materialdesignicons-webfont.woff2 +3 -0
- static/js/buefy.min.js +0 -0
- static/js/howler.min.js +4 -0
- static/js/vue.global.prod.js +0 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
static/fonts/materialdesignicons-webfont.ttf filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
static/fonts/materialdesignicons-webfont.woff filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
static/fonts/materialdesignicons-webfont.woff2 filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
CHANGED
|
@@ -27,5 +27,5 @@ ENV PORT=8000
|
|
| 27 |
# Expose the port the app runs on
|
| 28 |
EXPOSE 8000
|
| 29 |
|
| 30 |
-
# Command to run the application
|
| 31 |
CMD gunicorn main:app --workers 4 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:$PORT
|
|
|
|
| 27 |
# Expose the port the app runs on
|
| 28 |
EXPOSE 8000
|
| 29 |
|
| 30 |
+
# Command to run the application using uvicorn directly
|
| 31 |
CMD gunicorn main:app --workers 4 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:$PORT
|
README.md
CHANGED
|
@@ -1,10 +1,41 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# TranStudio
|
| 2 |
+
|
| 3 |
+
TranStudio is a web-based application for generating and managing audio transcriptions. It provides an intuitive interface for uploading audio files, generating transcriptions, and managing transcribed content with advanced playback controls.
|
| 4 |
+
|
| 5 |
+
## Features
|
| 6 |
+
|
| 7 |
+
- **Audio Upload**: Support for uploading audio files for transcription
|
| 8 |
+
- **Real-time Transcription**: Process audio files and generate text transcriptions
|
| 9 |
+
- **Interactive Audio Player**:
|
| 10 |
+
- Waveform visualization
|
| 11 |
+
- Play/pause controls
|
| 12 |
+
- Volume control with mute option
|
| 13 |
+
- Segment-based playback
|
| 14 |
+
- Time formatting and seeking
|
| 15 |
+
- **Transcription Management**:
|
| 16 |
+
- Save and load transcriptions
|
| 17 |
+
- Copy text to clipboard
|
| 18 |
+
- Delete segments
|
| 19 |
+
- View transcription history
|
| 20 |
+
- **Advanced Options**:
|
| 21 |
+
- Language selection
|
| 22 |
+
- Response format customization
|
| 23 |
+
- Temperature control
|
| 24 |
+
- Chunk size and overlap settings
|
| 25 |
+
|
| 26 |
+
## Technology Stack
|
| 27 |
+
|
| 28 |
+
- **Backend**: FastAPI (Python)
|
| 29 |
+
- **Frontend**: Vue.js with Buefy UI components
|
| 30 |
+
- **Audio Processing**:
|
| 31 |
+
- Howler.js for audio playback
|
| 32 |
+
- SoundFile for audio processing
|
| 33 |
+
- Custom waveform visualization
|
| 34 |
+
|
| 35 |
+
## Setup
|
| 36 |
+
|
| 37 |
+
1. Clone the repository
|
| 38 |
+
2. Install dependencies:
|
| 39 |
+
```bash
|
| 40 |
+
pip install -r requirements.txt
|
| 41 |
+
```
|
index.html
ADDED
|
@@ -0,0 +1,1406 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>TranStudio</title>
|
| 7 |
+
<link rel="stylesheet" href="/static/css/buefy.min.css">
|
| 8 |
+
<link rel="stylesheet" href="/static/css/materialdesignicons.min.css">
|
| 9 |
+
<script>
|
| 10 |
+
window.process = { env: { NODE_ENV: 'production' } };
|
| 11 |
+
</script>
|
| 12 |
+
<script src="/static/js/vue.global.prod.js"></script>
|
| 13 |
+
<script src="/static/js/buefy.min.js"></script>
|
| 14 |
+
<style>
|
| 15 |
+
.segments-container {
|
| 16 |
+
max-height: 300px;
|
| 17 |
+
overflow-y: auto;
|
| 18 |
+
border: 1px solid #ddd;
|
| 19 |
+
padding: 10px;
|
| 20 |
+
border-radius: 4px;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
.segment-box {
|
| 24 |
+
padding: 10px;
|
| 25 |
+
margin: 5px 0;
|
| 26 |
+
border: 1px solid #eee;
|
| 27 |
+
border-radius: 4px;
|
| 28 |
+
cursor: pointer;
|
| 29 |
+
transition: all 0.2s;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
.segment-box:hover {
|
| 33 |
+
background-color: #f5f5f5;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
.segment-box.is-active {
|
| 37 |
+
border-color: #3273dc;
|
| 38 |
+
background-color: #e8f0fe;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
.segment-time {
|
| 42 |
+
font-size: 0.8em;
|
| 43 |
+
color: #666;
|
| 44 |
+
margin-bottom: 5px;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
.segment-text {
|
| 48 |
+
margin: 5px 0;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
.segment-actions {
|
| 52 |
+
text-align: right;
|
| 53 |
+
}
|
| 54 |
+
.audio-player {
|
| 55 |
+
position: fixed;
|
| 56 |
+
bottom: 0;
|
| 57 |
+
left: 0;
|
| 58 |
+
right: 0;
|
| 59 |
+
background: white;
|
| 60 |
+
padding: 1rem;
|
| 61 |
+
box-shadow: 0 -2px 5px rgba(0,0,0,0.1);
|
| 62 |
+
z-index: 100;
|
| 63 |
+
margin-top: 2rem;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
.waveform-container {
|
| 67 |
+
margin: 1rem 0;
|
| 68 |
+
padding: 0 1rem;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.waveform {
|
| 72 |
+
position: relative;
|
| 73 |
+
height: 10vh;
|
| 74 |
+
background: #f0f0f0;
|
| 75 |
+
border-radius: 4px;
|
| 76 |
+
overflow: hidden;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
.progress {
|
| 80 |
+
position: absolute;
|
| 81 |
+
top: 0;
|
| 82 |
+
left: 0;
|
| 83 |
+
height: 100%;
|
| 84 |
+
background: rgba(50, 115, 220, 0.3);
|
| 85 |
+
pointer-events: none;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.player-controls {
|
| 89 |
+
display: flex;
|
| 90 |
+
align-items: center;
|
| 91 |
+
gap: 0.5rem;
|
| 92 |
+
margin-bottom: 0.5rem;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.segment-marker {
|
| 96 |
+
position: absolute;
|
| 97 |
+
height: 100%;
|
| 98 |
+
background: rgba(50, 115, 220, 0.2);
|
| 99 |
+
cursor: pointer;
|
| 100 |
+
transition: background 0.2s;
|
| 101 |
+
border-left: 1px solid #3273dc;
|
| 102 |
+
border-right: 1px solid #3273dc;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.segment-marker:hover {
|
| 106 |
+
background: rgba(50, 115, 220, 0.4);
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
.segment-tooltip {
|
| 110 |
+
position: absolute;
|
| 111 |
+
bottom: 100%;
|
| 112 |
+
background: #333;
|
| 113 |
+
color: white;
|
| 114 |
+
padding: 0.25rem 0.5rem;
|
| 115 |
+
border-radius: 3px;
|
| 116 |
+
font-size: 0.8rem;
|
| 117 |
+
white-space: nowrap;
|
| 118 |
+
display: none;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
.segment-marker:hover .segment-tooltip {
|
| 122 |
+
display: block;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.current-time {
|
| 126 |
+
text-align: center;
|
| 127 |
+
font-size: 0.9rem;
|
| 128 |
+
color: #666;
|
| 129 |
+
margin-top: 0.5rem;
|
| 130 |
+
}
|
| 131 |
+
.control-btn {
|
| 132 |
+
/* padding: 0.5rem; */
|
| 133 |
+
transition: all 0.2s;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
.control-btn:hover {
|
| 137 |
+
transform: scale(1.1);
|
| 138 |
+
}
|
| 139 |
+
.selection-info {
|
| 140 |
+
margin: 10px 0;
|
| 141 |
+
display: flex;
|
| 142 |
+
align-items: center;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
.slider-tick {
|
| 146 |
+
width: 2px;
|
| 147 |
+
height: 12px;
|
| 148 |
+
background-color: currentColor;
|
| 149 |
+
border-radius: 1px;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
/* Customize the range slider track */
|
| 153 |
+
.b-slider.is-info .b-slider-track {
|
| 154 |
+
background: rgba(50, 115, 220, 0.3);
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
/* Style for the selected range */
|
| 158 |
+
.b-slider.is-info .b-slider-fill {
|
| 159 |
+
background: #3273dc;
|
| 160 |
+
}
|
| 161 |
+
</style>
|
| 162 |
+
</head>
|
| 163 |
+
<body>
|
| 164 |
+
<div id="app" class="container">
|
| 165 |
+
<b-navbar>
|
| 166 |
+
<template #start>
|
| 167 |
+
<b-navbar-item @click="toggleSidebar">
|
| 168 |
+
<b-icon :icon="showSidebar ? 'menu-open' : 'menu'"></b-icon>
|
| 169 |
+
</b-navbar-item>
|
| 170 |
+
<b-navbar-item tag="router-link" :to="{ path: '/' }">
|
| 171 |
+
<a href="/">TranStudio</a>
|
| 172 |
+
</b-navbar-item>
|
| 173 |
+
</template>
|
| 174 |
+
<template #end>
|
| 175 |
+
<!-- <b-navbar-dropdown :label="isAuthenticated ? : 'Account'"> -->
|
| 176 |
+
<template v-if="isAuthenticated">
|
| 177 |
+
<b-navbar-item v-if="isAdmin" @click="showAdminPanel = true">
|
| 178 |
+
<b-icon icon="shield-account"></b-icon>
|
| 179 |
+
<span class="ml-1">Admin</span>
|
| 180 |
+
</b-navbar-item>
|
| 181 |
+
<b-navbar-item @click="logout">Logout {{ username }}</b-navbar-item>
|
| 182 |
+
</template>
|
| 183 |
+
<template v-else>
|
| 184 |
+
<b-navbar-item @click="showLoginModal = true">Login</b-navbar-item>
|
| 185 |
+
<b-navbar-item @click="showSignupModal = true">Sign Up</b-navbar-item>
|
| 186 |
+
</template>
|
| 187 |
+
<!-- </b-navbar-dropdown> -->
|
| 188 |
+
</template>
|
| 189 |
+
</b-navbar>
|
| 190 |
+
|
| 191 |
+
<div class="columns mb-6 pb-6">
|
| 192 |
+
<!-- Sidebar -->
|
| 193 |
+
<div class="column is-one-quarter" v-if="showSidebar">
|
| 194 |
+
<b-menu>
|
| 195 |
+
|
| 196 |
+
<b-menu-list :label="`Previous Transcriptions (${transcriptions.length})`">
|
| 197 |
+
<b-menu-item v-for="transcription in transcriptions"
|
| 198 |
+
class="is-flex is-justify-content-space-between is-align-items-center"
|
| 199 |
+
:key="transcription.id"
|
| 200 |
+
@click="loadTranscription(transcription)" :label="transcription.name">
|
| 201 |
+
|
| 202 |
+
<div class="is-flex is-justify-content-space-between is-align-items-center" style="width: 100%">
|
| 203 |
+
<span>
|
| 204 |
+
{{transcription?.audio_file}}
|
| 205 |
+
</span>
|
| 206 |
+
<b-button
|
| 207 |
+
type="is-danger"
|
| 208 |
+
size="is-small"
|
| 209 |
+
icon-left="delete"
|
| 210 |
+
@click.stop="deleteTranscription(transcription.id)">
|
| 211 |
+
</b-button>
|
| 212 |
+
</div>
|
| 213 |
+
</b-menu-item>
|
| 214 |
+
</b-menu-list>
|
| 215 |
+
</b-menu>
|
| 216 |
+
</div>
|
| 217 |
+
|
| 218 |
+
<!-- Main Content -->
|
| 219 |
+
<div class="column">
|
| 220 |
+
<!-- <b-field label="Select AI Model">
|
| 221 |
+
<b-select v-model="selectedModel">
|
| 222 |
+
<option value="whisper-large-v3-turbo">Groq - Whisper Large v3 Turbo</option>
|
| 223 |
+
<option value="openai-whisper-large-v3-turbo">Huggingface - OpenAI Whisper Large v3 Turbo</option>
|
| 224 |
+
</b-select>
|
| 225 |
+
</b-field> -->
|
| 226 |
+
|
| 227 |
+
<b-field label="Format">
|
| 228 |
+
<b-select v-model="responseFormat">
|
| 229 |
+
<option value="verbose_json">Verbose (with timestamps)</option>
|
| 230 |
+
<option value="json">Standard</option>
|
| 231 |
+
<option value="text">Simple</option>
|
| 232 |
+
</b-select>
|
| 233 |
+
</b-field>
|
| 234 |
+
|
| 235 |
+
<b-field label="Language">
|
| 236 |
+
<b-select v-model="selectedLanguage">
|
| 237 |
+
<option value="auto">Auto Detect</option>
|
| 238 |
+
<option value="en">English</option>
|
| 239 |
+
<option value="es">Spanish</option>
|
| 240 |
+
<option value="ar">Arabic</option>
|
| 241 |
+
<option value="fr">French</option>
|
| 242 |
+
<option value="de">German</option>
|
| 243 |
+
<option value="it">Italian</option>
|
| 244 |
+
</b-select>
|
| 245 |
+
</b-field>
|
| 246 |
+
|
| 247 |
+
<b-field label="Temperature">
|
| 248 |
+
<b-slider v-model="temperature" :min="0" :max="1" :step="0.1"></b-slider>
|
| 249 |
+
<span class="tag is-primary">{{ temperature }}</span>
|
| 250 |
+
</b-field>
|
| 251 |
+
|
| 252 |
+
<b-field label="Chunk Size (in minutes)">
|
| 253 |
+
<b-slider v-model="chunkSize" :min="5" :max="30" :step="1"></b-slider>
|
| 254 |
+
<span class="tag is-primary">{{ chunkSize }}</span>
|
| 255 |
+
</b-field>
|
| 256 |
+
|
| 257 |
+
<b-field label="Overlap (in seconds)">
|
| 258 |
+
<b-slider v-model="overlap" :min="2" :max="60" :step="1"></b-slider>
|
| 259 |
+
<span class="tag is-primary">{{ overlap }}</span>
|
| 260 |
+
</b-field>
|
| 261 |
+
|
| 262 |
+
<b-field label="Selection" v-if="totalDuration">
|
| 263 |
+
<b-slider v-model="selection" :step="1" type="is-info" :tooltip="false">
|
| 264 |
+
<!-- <template v-for="n in totalDuration" :key="n">
|
| 265 |
+
<b-slider-tick :value="n">{{ n }}</b-slider-tick>
|
| 266 |
+
</template> -->
|
| 267 |
+
</b-slider>
|
| 268 |
+
</b-field>
|
| 269 |
+
|
| 270 |
+
<div class="selection-info" v-if="totalDuration">
|
| 271 |
+
<span class="tag is-info">
|
| 272 |
+
Start: {{ formatTime(selection[0] * totalDuration / 100) }}
|
| 273 |
+
</span>
|
| 274 |
+
<span class="tag is-info ml-2">
|
| 275 |
+
End: {{ formatTime(selection[1] * totalDuration / 100) }}
|
| 276 |
+
</span>
|
| 277 |
+
</div>
|
| 278 |
+
|
| 279 |
+
<b-field label="System Prompt (Optional)">
|
| 280 |
+
<b-input v-model="systemPrompt" maxlength="256" type="textarea"></b-input>
|
| 281 |
+
</b-field>
|
| 282 |
+
|
| 283 |
+
<b-upload v-model="audioFile" accept="audio/*" @change="handleAudioUpload" multiple>
|
| 284 |
+
<a class="button is-primary">
|
| 285 |
+
<b-icon icon="upload"></b-icon>
|
| 286 |
+
<span>Click to upload</span>
|
| 287 |
+
</a>
|
| 288 |
+
</b-upload>
|
| 289 |
+
|
| 290 |
+
<b-field v-if="audioFile.length">
|
| 291 |
+
<span class="file-name">
|
| 292 |
+
{{ audioFile[0].name }}
|
| 293 |
+
</span>
|
| 294 |
+
</b-field>
|
| 295 |
+
</b-field>
|
| 296 |
+
|
| 297 |
+
<!-- <div v-if="audioUrl" class="audio-controls mb-4">
|
| 298 |
+
<audio ref="audioPlayer" :src="audioUrl"></audio>
|
| 299 |
+
<b-button @click="togglePlayPause">
|
| 300 |
+
{{ isPlaying ? 'Pause' : 'Play' }}
|
| 301 |
+
</b-button>
|
| 302 |
+
</div> -->
|
| 303 |
+
|
| 304 |
+
<b-field class="mt-2" label="Transcription">
|
| 305 |
+
<b-input v-model="transcriptionText" type="textarea" rows="5"></b-input>
|
| 306 |
+
</b-field>
|
| 307 |
+
|
| 308 |
+
<b-field class="mt-2" label="Segments" v-if="segments.length && responseFormat === 'verbose_json'">
|
| 309 |
+
<div class="segments-container">
|
| 310 |
+
<div v-for="(segment, index) in segments" :key="segment.id"
|
| 311 |
+
class="segment-box"
|
| 312 |
+
:class="{'is-active': activeSegment === index}"
|
| 313 |
+
@click="playSegment(segment)">
|
| 314 |
+
<div class="segment-time">
|
| 315 |
+
{{ formatTime(segment.start) }} - {{ formatTime(segment.end) }}
|
| 316 |
+
</div>
|
| 317 |
+
<div class="segment-text">
|
| 318 |
+
<b-input v-model="segment.text" type="textarea" rows="2"></b-input>
|
| 319 |
+
</div>
|
| 320 |
+
<div class="segment-actions">
|
| 321 |
+
<b-button type="is-info" size="is-small" @click.stop="copySegmentText(segment.text)">
|
| 322 |
+
<b-icon icon="content-copy"></b-icon>
|
| 323 |
+
</b-button>
|
| 324 |
+
<b-button type="is-danger" size="is-small" @click.stop="deleteSegment(index)">
|
| 325 |
+
<b-icon icon="delete"></b-icon>
|
| 326 |
+
</b-button>
|
| 327 |
+
</div>
|
| 328 |
+
</div>
|
| 329 |
+
</div>
|
| 330 |
+
</b-field>
|
| 331 |
+
|
| 332 |
+
<b-button type="is-primary" class="mr-2" @click="processTranscription" :disabled="!audioUrl" :loading="isProcessing">Process</b-button>
|
| 333 |
+
<b-button type="is-success" class="mr-2" @click="saveTranscription" :disabled="!transcriptionText" :loading="isProcessing">Save</b-button>
|
| 334 |
+
<b-button class="control-btn" @click="togglePlayer" :disabled="!audioUrl">
|
| 335 |
+
<b-icon :icon="showPlayer ? 'menu-down' : 'menu-up'"></b-icon>
|
| 336 |
+
</b-button>
|
| 337 |
+
</div>
|
| 338 |
+
|
| 339 |
+
</div><!-- .columns -->
|
| 340 |
+
|
| 341 |
+
<div class="columns mt-2">
|
| 342 |
+
<div class="audio-player" v-if="showPlayer">
|
| 343 |
+
<div class="player-controls" v-if="audioUrl">
|
| 344 |
+
<b-button class="control-btn" @click="togglePlay">
|
| 345 |
+
<b-icon :icon="isPlaying ? 'pause' : 'play'"></b-icon>
|
| 346 |
+
</b-button>
|
| 347 |
+
<b-button class="control-btn" @click="stopAudio">
|
| 348 |
+
<b-icon icon="stop"></b-icon>
|
| 349 |
+
</b-button>
|
| 350 |
+
<b-slider v-model="volume" :min="0" :max="1" :step="0.1" @input="updateVolume" size="is-small">
|
| 351 |
+
<template #indicator>
|
| 352 |
+
<span class="tag is-primary is-small">{{ volume.toFixed(1) }}</span>
|
| 353 |
+
</template>
|
| 354 |
+
</b-slider>
|
| 355 |
+
<b-button class="control-btn" @click="toggleMute">
|
| 356 |
+
<b-icon :icon="isMuted ? 'volume-off' : 'volume-high'"></b-icon>
|
| 357 |
+
</b-button>
|
| 358 |
+
<b-button class="control-btn" @click="togglePlayer">
|
| 359 |
+
<b-icon :icon="showPlayer ? 'menu-down' : 'menu-up'"></b-icon>
|
| 360 |
+
</b-button>
|
| 361 |
+
</div><!-- .player-controls -->
|
| 362 |
+
|
| 363 |
+
<div class="waveform-container" v-if="segments.length">
|
| 364 |
+
<div class="waveform" ref="waveform">
|
| 365 |
+
<div v-for="(segment, index) in segments"
|
| 366 |
+
:key="segment.id"
|
| 367 |
+
class="segment-marker"
|
| 368 |
+
:style="{ left: `${(segment.start / totalDuration) * 100}%`, width: `${((segment.end - segment.start) / totalDuration) * 100}%` }"
|
| 369 |
+
@click="playSegment(segment)">
|
| 370 |
+
<div class="segment-tooltip">
|
| 371 |
+
{{ formatTime(segment.start) }} - {{ formatTime(segment.end) }}
|
| 372 |
+
</div>
|
| 373 |
+
</div>
|
| 374 |
+
<div class="progress" :style="{ width: `${(currentTime / totalDuration) * 100}%` }"></div>
|
| 375 |
+
</div>
|
| 376 |
+
</div><!-- .waveform-container -->
|
| 377 |
+
|
| 378 |
+
<div class="current-time">
|
| 379 |
+
{{ formatTime(currentTime) }} / {{ formatTime(totalDuration) }}
|
| 380 |
+
</div>
|
| 381 |
+
|
| 382 |
+
</div><!-- .audio-player -->
|
| 383 |
+
</div>
|
| 384 |
+
|
| 385 |
+
|
| 386 |
+
<!-- Login Modal -->
|
| 387 |
+
<b-modal v-model="showLoginModal" has-modal-card trap-focus>
|
| 388 |
+
<div class="modal-card">
|
| 389 |
+
<header class="modal-card-head">
|
| 390 |
+
<p class="modal-card-title">Login</p>
|
| 391 |
+
</header>
|
| 392 |
+
<section class="modal-card-body">
|
| 393 |
+
<b-field label="Username">
|
| 394 |
+
<b-input type="text" v-model="loginForm.username" placeholder="Enter your username"></b-input>
|
| 395 |
+
</b-field>
|
| 396 |
+
<b-field label="Password">
|
| 397 |
+
<b-input type="password" v-model="loginForm.password" password-reveal></b-input>
|
| 398 |
+
</b-field>
|
| 399 |
+
</section>
|
| 400 |
+
<footer class="modal-card-foot">
|
| 401 |
+
<b-button type="is-primary" @click="handleLogin" :loading="isLoading">Login</b-button>
|
| 402 |
+
<b-button @click="showLoginModal = false">Cancel</b-button>
|
| 403 |
+
</footer>
|
| 404 |
+
</div>
|
| 405 |
+
</b-modal>
|
| 406 |
+
|
| 407 |
+
<!-- Signup Modal -->
|
| 408 |
+
<b-modal v-model="showSignupModal" has-modal-card trap-focus>
|
| 409 |
+
<div class="modal-card">
|
| 410 |
+
<header class="modal-card-head">
|
| 411 |
+
<p class="modal-card-title">Sign Up</p>
|
| 412 |
+
</header>
|
| 413 |
+
<section class="modal-card-body">
|
| 414 |
+
<b-field label="Username">
|
| 415 |
+
<b-input v-model="signupForm.username" placeholder="Enter username"></b-input>
|
| 416 |
+
</b-field>
|
| 417 |
+
<b-field label="Email">
|
| 418 |
+
<b-input type="email" v-model="signupForm.email" placeholder="Enter your email"></b-input>
|
| 419 |
+
</b-field>
|
| 420 |
+
<b-field label="Password">
|
| 421 |
+
<b-input type="password" v-model="signupForm.password" password-reveal></b-input>
|
| 422 |
+
</b-field>
|
| 423 |
+
<b-field label="Confirm Password">
|
| 424 |
+
<b-input type="password" v-model="signupForm.confirmPassword" password-reveal></b-input>
|
| 425 |
+
</b-field>
|
| 426 |
+
</section>
|
| 427 |
+
<footer class="modal-card-foot">
|
| 428 |
+
<b-button type="is-primary" @click="handleSignup" :loading="isLoading">Sign Up</b-button>
|
| 429 |
+
<b-button @click="showSignupModal = false">Cancel</b-button>
|
| 430 |
+
</footer>
|
| 431 |
+
</div>
|
| 432 |
+
</b-modal>
|
| 433 |
+
|
| 434 |
+
<!-- Admin Panel Modal -->
|
| 435 |
+
<b-modal v-model="showAdminPanel" has-modal-card trap-focus :width="640">
|
| 436 |
+
<div class="modal-card">
|
| 437 |
+
<header class="modal-card-head">
|
| 438 |
+
<p class="modal-card-title">User Management</p>
|
| 439 |
+
</header>
|
| 440 |
+
<section class="modal-card-body">
|
| 441 |
+
<div class="block">
|
| 442 |
+
<b-field grouped>
|
| 443 |
+
<b-input placeholder="Search users..." v-model="userSearchQuery" expanded></b-input>
|
| 444 |
+
<b-button type="is-primary" icon-left="magnify">Search</b-button>
|
| 445 |
+
</b-field>
|
| 446 |
+
</div>
|
| 447 |
+
|
| 448 |
+
<b-table
|
| 449 |
+
:data="filteredUsers"
|
| 450 |
+
:loading="loadingUsers"
|
| 451 |
+
:paginated="true"
|
| 452 |
+
:per-page="5"
|
| 453 |
+
:mobile-cards="true">
|
| 454 |
+
|
| 455 |
+
<b-table-column field="id" label="ID" width="40" numeric v-slot="props">
|
| 456 |
+
{{ props.row.id }}
|
| 457 |
+
</b-table-column>
|
| 458 |
+
|
| 459 |
+
<b-table-column field="username" label="Username" v-slot="props">
|
| 460 |
+
{{ props.row.username }}
|
| 461 |
+
</b-table-column>
|
| 462 |
+
|
| 463 |
+
<b-table-column field="email" label="Email" v-slot="props">
|
| 464 |
+
{{ props.row.email }}
|
| 465 |
+
</b-table-column>
|
| 466 |
+
|
| 467 |
+
<b-table-column field="is_admin" label="Admin" v-slot="props">
|
| 468 |
+
<b-icon
|
| 469 |
+
:icon="props.row.is_admin ? 'check' : 'close'"
|
| 470 |
+
:type="props.row.is_admin ? 'is-success' : 'is-danger'">
|
| 471 |
+
</b-icon>
|
| 472 |
+
</b-table-column>
|
| 473 |
+
|
| 474 |
+
<b-table-column field="disabled" label="Status" v-slot="props">
|
| 475 |
+
<b-tag :type="props.row.disabled ? 'is-danger' : 'is-success'">
|
| 476 |
+
{{ props.row.disabled ? 'Disabled' : 'Active' }}
|
| 477 |
+
</b-tag>
|
| 478 |
+
</b-table-column>
|
| 479 |
+
|
| 480 |
+
<b-table-column label="Actions" v-slot="props">
|
| 481 |
+
<div class="buttons">
|
| 482 |
+
<b-button
|
| 483 |
+
size="is-small"
|
| 484 |
+
:type="props.row.disabled ? 'is-success' : 'is-warning'"
|
| 485 |
+
:icon-left="props.row.disabled ? 'account-check' : 'account-cancel'"
|
| 486 |
+
@click="toggleUserStatus(props.row)"
|
| 487 |
+
:disabled="props.row.is_admin && props.row.id === currentUser.id">
|
| 488 |
+
{{ props.row.disabled ? 'Enable' : 'Disable' }}
|
| 489 |
+
</b-button>
|
| 490 |
+
<b-button
|
| 491 |
+
size="is-small"
|
| 492 |
+
type="is-danger"
|
| 493 |
+
icon-left="delete"
|
| 494 |
+
@click="deleteUser(props.row)"
|
| 495 |
+
:disabled="props.row.id === currentUser.id">
|
| 496 |
+
Delete
|
| 497 |
+
</b-button>
|
| 498 |
+
</div>
|
| 499 |
+
</b-table-column>
|
| 500 |
+
|
| 501 |
+
<template #empty>
|
| 502 |
+
<div class="has-text-centered">No users found</div>
|
| 503 |
+
</template>
|
| 504 |
+
</b-table>
|
| 505 |
+
</section>
|
| 506 |
+
<footer class="modal-card-foot">
|
| 507 |
+
<b-button @click="refreshUsers" type="is-info" icon-left="refresh">Refresh</b-button>
|
| 508 |
+
<b-button @click="showAdminPanel = false">Close</b-button>
|
| 509 |
+
</footer>
|
| 510 |
+
</div>
|
| 511 |
+
</b-modal>
|
| 512 |
+
|
| 513 |
+
</div><!-- #app -->
|
| 514 |
+
|
| 515 |
+
<script src="/static/js/howler.min.js"></script>
|
| 516 |
+
<script>
|
| 517 |
+
const { createApp, ref, onMounted } = Vue;
|
| 518 |
+
const app = createApp({
|
| 519 |
+
data() {
|
| 520 |
+
return {
|
| 521 |
+
responseFormat: 'verbose_json',
|
| 522 |
+
temperature: 0,
|
| 523 |
+
chunkSize: 10,
|
| 524 |
+
overlap: 5,
|
| 525 |
+
selection: [1,100],
|
| 526 |
+
systemPrompt: '',
|
| 527 |
+
isAuthenticated: false,
|
| 528 |
+
username: '',
|
| 529 |
+
token: '',
|
| 530 |
+
isLoading: false,
|
| 531 |
+
audioFile: [],
|
| 532 |
+
segments: [],
|
| 533 |
+
transcriptionText: '',
|
| 534 |
+
selectedLanguage: 'auto',
|
| 535 |
+
transcriptions: [],
|
| 536 |
+
showSidebar: true,
|
| 537 |
+
showPlayer: false,
|
| 538 |
+
showApiKeyModal: false,
|
| 539 |
+
audioUrl: null,
|
| 540 |
+
isPlaying: false,
|
| 541 |
+
audioPlayer: null,
|
| 542 |
+
isProcessing: false,
|
| 543 |
+
howl: null,
|
| 544 |
+
activeSegment: null,
|
| 545 |
+
isPlayingSegment: false,
|
| 546 |
+
isMuted: false,
|
| 547 |
+
volume: 0.5,
|
| 548 |
+
currentTime: 0,
|
| 549 |
+
totalDuration: 0,
|
| 550 |
+
seekInterval: null,
|
| 551 |
+
waveformData: null,
|
| 552 |
+
waveformCanvas: null,
|
| 553 |
+
ctx: null,
|
| 554 |
+
showLoginModal: false,
|
| 555 |
+
loginForm: {
|
| 556 |
+
username: '',
|
| 557 |
+
password: '',
|
| 558 |
+
},
|
| 559 |
+
showSignupModal: false,
|
| 560 |
+
signupForm: {
|
| 561 |
+
username: '',
|
| 562 |
+
email: '',
|
| 563 |
+
password: '',
|
| 564 |
+
confirmPassword: ''
|
| 565 |
+
},
|
| 566 |
+
// Admin panel data
|
| 567 |
+
showAdminPanel: false,
|
| 568 |
+
isAdmin: false,
|
| 569 |
+
currentUser: null,
|
| 570 |
+
users: [],
|
| 571 |
+
loadingUsers: false,
|
| 572 |
+
userSearchQuery: '',
|
| 573 |
+
// processingProgress: 0,
|
| 574 |
+
// totalChunks: 0,
|
| 575 |
+
// showVolumeSlider: false
|
| 576 |
+
}
|
| 577 |
+
},
|
| 578 |
+
|
| 579 |
+
methods: {
|
| 580 |
+
async login() {
|
| 581 |
+
this.isAuthenticated = true;
|
| 582 |
+
},
|
| 583 |
+
|
| 584 |
+
logout() {
|
| 585 |
+
this.token = '';
|
| 586 |
+
this.username = '';
|
| 587 |
+
this.isAuthenticated = false;
|
| 588 |
+
this.isAdmin = false;
|
| 589 |
+
this.currentUser = null;
|
| 590 |
+
this.users = [];
|
| 591 |
+
this.transcriptions = [];
|
| 592 |
+
localStorage.removeItem('token');
|
| 593 |
+
|
| 594 |
+
// Reset UI state
|
| 595 |
+
this.audioUrl = null;
|
| 596 |
+
this.audioFile = [];
|
| 597 |
+
this.segments = [];
|
| 598 |
+
this.transcriptionText = '';
|
| 599 |
+
|
| 600 |
+
// Show notification
|
| 601 |
+
this.$buefy.toast.open({
|
| 602 |
+
message: 'Successfully logged out!',
|
| 603 |
+
type: 'is-success'
|
| 604 |
+
});
|
| 605 |
+
|
| 606 |
+
// Redirect to home page if not already there
|
| 607 |
+
if (window.location.pathname !== '/') {
|
| 608 |
+
window.location.href = '/';
|
| 609 |
+
} else {
|
| 610 |
+
// window.location.reload();
|
| 611 |
+
}
|
| 612 |
+
},
|
| 613 |
+
|
| 614 |
+
async handleAudioUpload(event) {
|
| 615 |
+
if (!event.target.files || !event.target.files.length) return;
|
| 616 |
+
|
| 617 |
+
const file = event.target.files[0];
|
| 618 |
+
const fileUrl = URL.createObjectURL(file);
|
| 619 |
+
fileExt = file.name.split('.').splice(-1);
|
| 620 |
+
|
| 621 |
+
this.howl = new Howl({
|
| 622 |
+
src: [fileUrl],
|
| 623 |
+
format: this.fileExt, // This should be an array
|
| 624 |
+
html5: true,
|
| 625 |
+
onend: () => {
|
| 626 |
+
this.isPlayingSegment = false;
|
| 627 |
+
this.activeSegment = null;
|
| 628 |
+
}
|
| 629 |
+
});
|
| 630 |
+
|
| 631 |
+
this.audioUrl = fileUrl;
|
| 632 |
+
this.audioFile = [file];
|
| 633 |
+
},
|
| 634 |
+
|
| 635 |
+
initializeAudio() {
|
| 636 |
+
if (this.howl) {
|
| 637 |
+
this.howl.unload();
|
| 638 |
+
}
|
| 639 |
+
|
| 640 |
+
const fileExt = this.audioFile[0].name.split('.').splice(-1);
|
| 641 |
+
this.howl = new Howl({
|
| 642 |
+
src: [this.audioUrl],
|
| 643 |
+
format: fileExt,
|
| 644 |
+
html5: true,
|
| 645 |
+
volume: this.volume,
|
| 646 |
+
sprite: this.createSprites(),
|
| 647 |
+
onplay: () => {
|
| 648 |
+
this.isPlaying = true;
|
| 649 |
+
this.startSeekUpdate();
|
| 650 |
+
},
|
| 651 |
+
onpause: () => {
|
| 652 |
+
this.isPlaying = false;
|
| 653 |
+
this.stopSeekUpdate();
|
| 654 |
+
},
|
| 655 |
+
onstop: () => {
|
| 656 |
+
this.isPlaying = false;
|
| 657 |
+
this.currentTime = 0;
|
| 658 |
+
this.stopSeekUpdate();
|
| 659 |
+
},
|
| 660 |
+
onend: () => {
|
| 661 |
+
this.isPlaying = false;
|
| 662 |
+
this.currentTime = 0;
|
| 663 |
+
this.stopSeekUpdate();
|
| 664 |
+
},
|
| 665 |
+
onload: () => {
|
| 666 |
+
this.totalDuration = this.howl.duration();
|
| 667 |
+
/*/ Set selection to the length of the audio file
|
| 668 |
+
this.selection = [];
|
| 669 |
+
for (let i = 0; i < this.totalDuration; i += this.chunkSize) {
|
| 670 |
+
this.selection.push(i);
|
| 671 |
+
}*/
|
| 672 |
+
}
|
| 673 |
+
});
|
| 674 |
+
},
|
| 675 |
+
|
| 676 |
+
async initializeWaveform() {
|
| 677 |
+
if (!this.audioUrl) return;
|
| 678 |
+
|
| 679 |
+
// Load audio file and decode it
|
| 680 |
+
const response = await fetch(this.audioUrl);
|
| 681 |
+
const arrayBuffer = await response.arrayBuffer();
|
| 682 |
+
const audioContext = new AudioContext();
|
| 683 |
+
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
| 684 |
+
|
| 685 |
+
// Get waveform data
|
| 686 |
+
const channelData = audioBuffer.getChannelData(0);
|
| 687 |
+
this.waveformData = this.processWaveformData(channelData);
|
| 688 |
+
|
| 689 |
+
// Draw waveform
|
| 690 |
+
this.$nextTick(() => {
|
| 691 |
+
this.drawWaveform();
|
| 692 |
+
});
|
| 693 |
+
},
|
| 694 |
+
|
| 695 |
+
processWaveformData(data) {
|
| 696 |
+
const step = Math.ceil(data.length / 1000);
|
| 697 |
+
const waveform = [];
|
| 698 |
+
for (let i = 0; i < data.length; i += step) {
|
| 699 |
+
const slice = data.slice(i, i + step);
|
| 700 |
+
// Use reducer instead of spread operator for large arrays
|
| 701 |
+
const max = slice.reduce((a, b) => Math.max(a, b), -Infinity);
|
| 702 |
+
const min = slice.reduce((a, b) => Math.min(a, b), Infinity);
|
| 703 |
+
waveform.push({ max, min });
|
| 704 |
+
}
|
| 705 |
+
return waveform;
|
| 706 |
+
},
|
| 707 |
+
|
| 708 |
+
drawWaveform() {
|
| 709 |
+
if (this.$refs.waveform){
|
| 710 |
+
const canvas = document.createElement('canvas');
|
| 711 |
+
const ctx = canvas.getContext('2d');
|
| 712 |
+
const width = this.$refs.waveform.offsetWidth;
|
| 713 |
+
const height = 100;
|
| 714 |
+
|
| 715 |
+
canvas.width = width;
|
| 716 |
+
canvas.height = height;
|
| 717 |
+
this.$refs.waveform.style.backgroundImage = `url(${canvas.toDataURL()})`;
|
| 718 |
+
|
| 719 |
+
ctx.fillStyle = '#3273dc';
|
| 720 |
+
this.waveformData.forEach((point, i) => {
|
| 721 |
+
const x = (i / this.waveformData.length) * width;
|
| 722 |
+
const y = (1 - point.max) * height / 2;
|
| 723 |
+
const h = (point.max - point.min) * height / 2;
|
| 724 |
+
ctx.fillRect(x, y, 1, h);
|
| 725 |
+
});
|
| 726 |
+
}
|
| 727 |
+
},
|
| 728 |
+
|
| 729 |
+
createSprites() {
|
| 730 |
+
const sprites = {};
|
| 731 |
+
this.segments.forEach((segment, index) => {
|
| 732 |
+
sprites[`segment_${index}`] = [segment.start * 1000, (segment.end - segment.start) * 1000];
|
| 733 |
+
});
|
| 734 |
+
return sprites;
|
| 735 |
+
},
|
| 736 |
+
|
| 737 |
+
togglePlay() {
|
| 738 |
+
if (this.isPlaying) {
|
| 739 |
+
this.howl.pause();
|
| 740 |
+
} else {
|
| 741 |
+
this.howl.play();
|
| 742 |
+
}
|
| 743 |
+
},
|
| 744 |
+
|
| 745 |
+
stopAudio() {
|
| 746 |
+
this.howl.stop();
|
| 747 |
+
},
|
| 748 |
+
|
| 749 |
+
updateVolume() {
|
| 750 |
+
if (this.volume <= 0) {
|
| 751 |
+
this.isMuted = true;
|
| 752 |
+
this.howl.mute(true);
|
| 753 |
+
} else if (this.isMuted && this.volume > 0) {
|
| 754 |
+
this.isMuted = false;
|
| 755 |
+
this.howl.mute(false);
|
| 756 |
+
}
|
| 757 |
+
this.howl.volume(this.volume);
|
| 758 |
+
},
|
| 759 |
+
|
| 760 |
+
toggleVolumeSlider() {
|
| 761 |
+
this.showVolumeSlider = !this.showVolumeSlider;
|
| 762 |
+
},
|
| 763 |
+
|
| 764 |
+
toggleMute() {
|
| 765 |
+
this.isMuted = !this.isMuted;
|
| 766 |
+
this.howl.mute(this.isMuted);
|
| 767 |
+
if (!this.isMuted && this.volume === 0) {
|
| 768 |
+
this.volume = 0.5;
|
| 769 |
+
}
|
| 770 |
+
},
|
| 771 |
+
|
| 772 |
+
playSegment(segment) {
|
| 773 |
+
const index = this.segments.indexOf(segment);
|
| 774 |
+
if (index !== -1) {
|
| 775 |
+
this.howl.play(`segment_${index}`);
|
| 776 |
+
}
|
| 777 |
+
},
|
| 778 |
+
|
| 779 |
+
startSeekUpdate() {
|
| 780 |
+
this.seekInterval = setInterval(() => {
|
| 781 |
+
this.currentTime = this.howl.seek() || 0;
|
| 782 |
+
}, 100);
|
| 783 |
+
},
|
| 784 |
+
|
| 785 |
+
stopSeekUpdate() {
|
| 786 |
+
clearInterval(this.seekInterval);
|
| 787 |
+
},
|
| 788 |
+
|
| 789 |
+
formatTime(seconds) {
|
| 790 |
+
const mins = Math.floor(seconds / 60);
|
| 791 |
+
const secs = Math.floor(seconds % 60);
|
| 792 |
+
return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
|
| 793 |
+
},
|
| 794 |
+
|
| 795 |
+
togglePlayPause() {
|
| 796 |
+
if (this.audioPlayer.paused) {
|
| 797 |
+
this.audioPlayer.play();
|
| 798 |
+
this.isPlaying = true;
|
| 799 |
+
} else {
|
| 800 |
+
this.audioPlayer.pause();
|
| 801 |
+
this.isPlaying = false;
|
| 802 |
+
}
|
| 803 |
+
},
|
| 804 |
+
|
| 805 |
+
playSegment(segment) {
|
| 806 |
+
if (!this.howl) return;
|
| 807 |
+
|
| 808 |
+
if (this.activeSegment === segment.id && this.isPlayingSegment) {
|
| 809 |
+
this.howl.pause();
|
| 810 |
+
this.isPlayingSegment = false;
|
| 811 |
+
return;
|
| 812 |
+
}
|
| 813 |
+
|
| 814 |
+
this.howl.seek(segment.start);
|
| 815 |
+
this.howl.play();
|
| 816 |
+
this.activeSegment = segment.id;
|
| 817 |
+
this.isPlayingSegment = true;
|
| 818 |
+
|
| 819 |
+
this.howl.once('end', () => {
|
| 820 |
+
this.isPlayingSegment = false;
|
| 821 |
+
this.activeSegment = null;
|
| 822 |
+
});
|
| 823 |
+
},
|
| 824 |
+
|
| 825 |
+
copySegmentText(text) {
|
| 826 |
+
navigator.clipboard.writeText(text).then(() => {
|
| 827 |
+
this.$buefy.toast.open({
|
| 828 |
+
message: 'Text copied to clipboard',
|
| 829 |
+
type: 'is-success',
|
| 830 |
+
position: 'is-bottom-right'
|
| 831 |
+
});
|
| 832 |
+
}).catch(() => {
|
| 833 |
+
this.$buefy.toast.open({
|
| 834 |
+
message: 'Failed to copy text',
|
| 835 |
+
type: 'is-danger',
|
| 836 |
+
position: 'is-bottom-right'
|
| 837 |
+
});
|
| 838 |
+
});
|
| 839 |
+
},
|
| 840 |
+
|
| 841 |
+
deleteSegment(index) {
|
| 842 |
+
this.segments.splice(index, 1);
|
| 843 |
+
this.updateTranscriptionText();
|
| 844 |
+
},
|
| 845 |
+
|
| 846 |
+
updateTranscriptionText() {
|
| 847 |
+
this.transcriptionText = this.segments
|
| 848 |
+
.map(segment => segment.text)
|
| 849 |
+
.join(' ');
|
| 850 |
+
},
|
| 851 |
+
|
| 852 |
+
formatTime(seconds) {
|
| 853 |
+
const mins = Math.floor(seconds / 60);
|
| 854 |
+
const secs = Math.floor(seconds % 60);
|
| 855 |
+
return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
|
| 856 |
+
},
|
| 857 |
+
|
| 858 |
+
async processTranscription() {
|
| 859 |
+
const formData = new FormData();
|
| 860 |
+
formData.append('file', this.audioFile[0]);
|
| 861 |
+
formData.append('response_format', this.responseFormat);
|
| 862 |
+
formData.append('temperature', this.temperature.toString());
|
| 863 |
+
formData.append('chunk_size', parseInt(this.chunkSize * 60).toString());
|
| 864 |
+
formData.append('overlap', this.overlap.toString());
|
| 865 |
+
formData.append('prompt', this.systemPrompt.toString());
|
| 866 |
+
|
| 867 |
+
// Add selection timestamps
|
| 868 |
+
const startTime = (this.selection[0] * this.totalDuration / 100).toFixed(2);
|
| 869 |
+
const endTime = (this.selection[1] * this.totalDuration / 100).toFixed(2);
|
| 870 |
+
formData.append('start_time', startTime);
|
| 871 |
+
formData.append('end_time', endTime);
|
| 872 |
+
|
| 873 |
+
if (this.selectedLanguage !== 'auto') {
|
| 874 |
+
formData.append('language', this.selectedLanguage);
|
| 875 |
+
}
|
| 876 |
+
try {
|
| 877 |
+
this.isProcessing = true;
|
| 878 |
+
|
| 879 |
+
/*/ Monitor the progress of the upload for user feedback (need to be updated for reading back response)
|
| 880 |
+
const xhr = new XMLHttpRequest();
|
| 881 |
+
xhr.upload.onloadstart = function (event) {
|
| 882 |
+
console.log('Upload started');
|
| 883 |
+
};
|
| 884 |
+
|
| 885 |
+
xhr.upload.onprogress = function (event) {
|
| 886 |
+
if (event.lengthComputable) {
|
| 887 |
+
const percentComplete = (event.loaded / event.total) * 100;
|
| 888 |
+
console.log(`Upload progress: ${percentComplete.toFixed(2)}%`);
|
| 889 |
+
}
|
| 890 |
+
};
|
| 891 |
+
xhr.upload.onload = function () {
|
| 892 |
+
console.log('Upload complete');
|
| 893 |
+
};
|
| 894 |
+
xhr.onerror = function () {
|
| 895 |
+
console.error('Error uploading file');
|
| 896 |
+
};
|
| 897 |
+
|
| 898 |
+
xhr.open('POST', '/api/upload', true);
|
| 899 |
+
xhr.send(formData);*/
|
| 900 |
+
|
| 901 |
+
const response = await fetch('/api/upload', {
|
| 902 |
+
method: 'POST',
|
| 903 |
+
headers: {
|
| 904 |
+
'Authorization': `Bearer ${this.token}`
|
| 905 |
+
},
|
| 906 |
+
body: formData
|
| 907 |
+
});
|
| 908 |
+
if (!response.ok) {
|
| 909 |
+
const errorData = await response.json();
|
| 910 |
+
throw new Error(errorData.detail || 'Transcription failed');
|
| 911 |
+
}
|
| 912 |
+
const result = await response.json();
|
| 913 |
+
|
| 914 |
+
// Check for backend validation errors
|
| 915 |
+
if (result.metadata?.errors?.length) {
|
| 916 |
+
console.error('Chunk processing errors:', result.metadata.errors);
|
| 917 |
+
this.$buefy.snackbar.open({
|
| 918 |
+
message: `${result.metadata.errors.length} chunks failed to process`,
|
| 919 |
+
type: 'is-warning',
|
| 920 |
+
position: 'is-bottom-right'
|
| 921 |
+
});
|
| 922 |
+
}
|
| 923 |
+
|
| 924 |
+
if (this.responseFormat === 'verbose_json') {
|
| 925 |
+
|
| 926 |
+
// Validate segments
|
| 927 |
+
if (!result.segments) {
|
| 928 |
+
throw new Error('Server returned invalid segments format');
|
| 929 |
+
}
|
| 930 |
+
|
| 931 |
+
this.segments = result.segments;
|
| 932 |
+
this.updateTranscriptionText();
|
| 933 |
+
} else {
|
| 934 |
+
this.transcriptionText = result.text || result;
|
| 935 |
+
}
|
| 936 |
+
|
| 937 |
+
// this.totalChunks = result.metadata?.total_chunks || 0;
|
| 938 |
+
// this.processingProgress = ((processedChunks / this.totalChunks) * 100).toFixed(1);
|
| 939 |
+
} catch (error) {
|
| 940 |
+
console.error('Transcription error:', error);
|
| 941 |
+
this.$buefy.toast.open({
|
| 942 |
+
message: `Error: ${error.message}`,
|
| 943 |
+
type: 'is-danger',
|
| 944 |
+
position: 'is-bottom-right'
|
| 945 |
+
});
|
| 946 |
+
}
|
| 947 |
+
this.isProcessing = false;
|
| 948 |
+
this.showPlayer = true;
|
| 949 |
+
},
|
| 950 |
+
|
| 951 |
+
async loadTranscriptions() {
|
| 952 |
+
try {
|
| 953 |
+
// Check if token exists
|
| 954 |
+
if (!this.token) {
|
| 955 |
+
this.token = localStorage.getItem('token');
|
| 956 |
+
if (!this.token) {
|
| 957 |
+
// console.log('No authentication token found');
|
| 958 |
+
return;
|
| 959 |
+
}
|
| 960 |
+
}
|
| 961 |
+
|
| 962 |
+
const response = await fetch('/api/transcriptions', {
|
| 963 |
+
headers: {
|
| 964 |
+
'Authorization': `Bearer ${this.token}`
|
| 965 |
+
}
|
| 966 |
+
});
|
| 967 |
+
|
| 968 |
+
if (response.status === 401) {
|
| 969 |
+
// Token expired or invalid
|
| 970 |
+
console.log('Authentication token expired or invalid');
|
| 971 |
+
this.logout();
|
| 972 |
+
this.$buefy.toast.open({
|
| 973 |
+
message: 'Your session has expired. Please login again.',
|
| 974 |
+
type: 'is-warning'
|
| 975 |
+
});
|
| 976 |
+
return;
|
| 977 |
+
}
|
| 978 |
+
|
| 979 |
+
if (!response.ok) {
|
| 980 |
+
throw new Error(`HTTP error! Status: ${response.status}`);
|
| 981 |
+
}
|
| 982 |
+
|
| 983 |
+
this.transcriptions = await response.json();
|
| 984 |
+
} catch (error) {
|
| 985 |
+
console.error('Error loading transcriptions:', error);
|
| 986 |
+
this.$buefy.toast.open({
|
| 987 |
+
message: `Failed to load transcriptions: ${error.message}`,
|
| 988 |
+
type: 'is-danger'
|
| 989 |
+
});
|
| 990 |
+
}
|
| 991 |
+
},
|
| 992 |
+
|
| 993 |
+
async loadTranscription(transcription) {
|
| 994 |
+
try {
|
| 995 |
+
const response = await fetch(`/api/transcriptions/${transcription.id}`, {
|
| 996 |
+
headers: {
|
| 997 |
+
'Authorization': `Bearer ${this.token}`
|
| 998 |
+
}
|
| 999 |
+
});
|
| 1000 |
+
const data = await response.json();
|
| 1001 |
+
// Set the audio file for playback
|
| 1002 |
+
this.transcriptionText = data['text'];
|
| 1003 |
+
this.segments = data['segments'];
|
| 1004 |
+
// this.audioFile = data['audio_file'];
|
| 1005 |
+
if (data.audio_file) {
|
| 1006 |
+
// this.audioUrl = `/uploads/${data['audio_file']}`;
|
| 1007 |
+
// this.audioFile.push(data['audio_file']);
|
| 1008 |
+
// this.initializeAudio();
|
| 1009 |
+
}
|
| 1010 |
+
} catch (error) {
|
| 1011 |
+
console.error('Error loading transcription:', error);
|
| 1012 |
+
}
|
| 1013 |
+
},
|
| 1014 |
+
|
| 1015 |
+
async saveTranscription() {
|
| 1016 |
+
if (this.audioFile.length == 0){
|
| 1017 |
+
this.$buefy.toast.open({
|
| 1018 |
+
message: 'Please upload an audio file first',
|
| 1019 |
+
type: 'is-warning',
|
| 1020 |
+
position: 'is-bottom-right'
|
| 1021 |
+
});
|
| 1022 |
+
return;
|
| 1023 |
+
}
|
| 1024 |
+
try {
|
| 1025 |
+
this.isProcessing = true;
|
| 1026 |
+
const response = await fetch('/api/save-transcription', {
|
| 1027 |
+
method: 'POST',
|
| 1028 |
+
headers: {
|
| 1029 |
+
'Content-Type': 'application/json',
|
| 1030 |
+
'Authorization': `Bearer ${this.token}`
|
| 1031 |
+
},
|
| 1032 |
+
body: JSON.stringify({
|
| 1033 |
+
text: this.transcriptionText,
|
| 1034 |
+
segments: this.segments,
|
| 1035 |
+
audio_file: this.audioFile[0].name
|
| 1036 |
+
})
|
| 1037 |
+
});
|
| 1038 |
+
|
| 1039 |
+
if (!response.ok) throw new Error('Failed to save transcription');
|
| 1040 |
+
|
| 1041 |
+
await this.loadTranscriptions();
|
| 1042 |
+
this.$buefy.toast.open({
|
| 1043 |
+
message: 'Transcription saved successfully',
|
| 1044 |
+
type: 'is-success',
|
| 1045 |
+
position: 'is-bottom-right'
|
| 1046 |
+
});
|
| 1047 |
+
} catch (error) {
|
| 1048 |
+
console.error('Error saving transcription:', error);
|
| 1049 |
+
this.$buefy.toast.open({
|
| 1050 |
+
message: `Error: ${error.message}`,
|
| 1051 |
+
type: 'is-danger',
|
| 1052 |
+
position: 'is-bottom-right'
|
| 1053 |
+
});
|
| 1054 |
+
}
|
| 1055 |
+
this.isProcessing = false;
|
| 1056 |
+
},
|
| 1057 |
+
|
| 1058 |
+
toggleSidebar() {
|
| 1059 |
+
this.showSidebar = !this.showSidebar;
|
| 1060 |
+
},
|
| 1061 |
+
togglePlayer() {
|
| 1062 |
+
this.showPlayer = !this.showPlayer;
|
| 1063 |
+
},
|
| 1064 |
+
async deleteTranscription(id) {
|
| 1065 |
+
try {
|
| 1066 |
+
// Show confirmation dialog
|
| 1067 |
+
this.$buefy.dialog.confirm({
|
| 1068 |
+
title: 'Delete Transcription',
|
| 1069 |
+
message: 'Are you sure you want to delete this transcription? This action cannot be undone.',
|
| 1070 |
+
confirmText: 'Delete',
|
| 1071 |
+
type: 'is-danger',
|
| 1072 |
+
hasIcon: true,
|
| 1073 |
+
onConfirm: async () => {
|
| 1074 |
+
const response = await fetch(`/api/transcriptions/${id}`, {
|
| 1075 |
+
method: 'DELETE',
|
| 1076 |
+
headers: {
|
| 1077 |
+
'Authorization': `Bearer ${this.token}`
|
| 1078 |
+
}
|
| 1079 |
+
});
|
| 1080 |
+
|
| 1081 |
+
if (!response.ok) throw new Error('Failed to delete transcription');
|
| 1082 |
+
|
| 1083 |
+
// Remove from local list
|
| 1084 |
+
this.transcriptions = this.transcriptions.filter(t => t.id !== id);
|
| 1085 |
+
|
| 1086 |
+
// Show success message
|
| 1087 |
+
this.$buefy.toast.open({
|
| 1088 |
+
message: 'Transcription deleted successfully',
|
| 1089 |
+
type: 'is-success',
|
| 1090 |
+
position: 'is-bottom-right'
|
| 1091 |
+
});
|
| 1092 |
+
}
|
| 1093 |
+
});
|
| 1094 |
+
} catch (error) {
|
| 1095 |
+
console.error('Error deleting transcription:', error);
|
| 1096 |
+
this.$buefy.toast.open({
|
| 1097 |
+
message: `Error: ${error.message}`,
|
| 1098 |
+
type: 'is-danger',
|
| 1099 |
+
position: 'is-bottom-right'
|
| 1100 |
+
});
|
| 1101 |
+
}
|
| 1102 |
+
},
|
| 1103 |
+
|
| 1104 |
+
async handleLogin() {
|
| 1105 |
+
try {
|
| 1106 |
+
this.isLoading = true;
|
| 1107 |
+
const formData = new FormData();
|
| 1108 |
+
formData.append('username', this.loginForm.username); // Make sure this matches the backend expectation
|
| 1109 |
+
formData.append('password', this.loginForm.password);
|
| 1110 |
+
|
| 1111 |
+
const response = await fetch('/token', {
|
| 1112 |
+
method: 'POST',
|
| 1113 |
+
body: formData
|
| 1114 |
+
});
|
| 1115 |
+
|
| 1116 |
+
if (!response.ok) {
|
| 1117 |
+
const errorData = await response.json();
|
| 1118 |
+
throw new Error(errorData.detail || 'Login failed');
|
| 1119 |
+
}
|
| 1120 |
+
|
| 1121 |
+
const data = await response.json();
|
| 1122 |
+
this.token = data.access_token;
|
| 1123 |
+
localStorage.setItem('token', data.access_token);
|
| 1124 |
+
|
| 1125 |
+
// Get user info
|
| 1126 |
+
const userResponse = await fetch('/api/me', {
|
| 1127 |
+
headers: {
|
| 1128 |
+
'Authorization': `Bearer ${this.token}`
|
| 1129 |
+
}
|
| 1130 |
+
});
|
| 1131 |
+
|
| 1132 |
+
if (!userResponse.ok) {
|
| 1133 |
+
throw new Error('Failed to get user information');
|
| 1134 |
+
}
|
| 1135 |
+
|
| 1136 |
+
const userData = await userResponse.json();
|
| 1137 |
+
this.username = userData.username;
|
| 1138 |
+
this.isAuthenticated = true;
|
| 1139 |
+
|
| 1140 |
+
this.showLoginModal = false;
|
| 1141 |
+
this.$buefy.toast.open({
|
| 1142 |
+
message: 'Successfully logged in!',
|
| 1143 |
+
type: 'is-success'
|
| 1144 |
+
});
|
| 1145 |
+
|
| 1146 |
+
// Load user data after successful login
|
| 1147 |
+
this.loadTranscriptions();
|
| 1148 |
+
} catch (error) {
|
| 1149 |
+
this.$buefy.toast.open({
|
| 1150 |
+
message: `Error: ${error.message}`,
|
| 1151 |
+
type: 'is-danger'
|
| 1152 |
+
});
|
| 1153 |
+
} finally {
|
| 1154 |
+
this.isLoading = false;
|
| 1155 |
+
}
|
| 1156 |
+
},
|
| 1157 |
+
|
| 1158 |
+
async handleSignup() {
|
| 1159 |
+
try {
|
| 1160 |
+
this.isLoading = true;
|
| 1161 |
+
if (this.signupForm.password !== this.signupForm.confirmPassword) {
|
| 1162 |
+
throw new Error('Passwords do not match');
|
| 1163 |
+
}
|
| 1164 |
+
|
| 1165 |
+
const response = await fetch('/api/signup', {
|
| 1166 |
+
method: 'POST',
|
| 1167 |
+
headers: {
|
| 1168 |
+
'Content-Type': 'application/json'
|
| 1169 |
+
},
|
| 1170 |
+
body: JSON.stringify({
|
| 1171 |
+
username: this.signupForm.username,
|
| 1172 |
+
email: this.signupForm.email,
|
| 1173 |
+
password: this.signupForm.password
|
| 1174 |
+
})
|
| 1175 |
+
});
|
| 1176 |
+
|
| 1177 |
+
if (!response.ok) {
|
| 1178 |
+
const error = await response.json();
|
| 1179 |
+
throw new Error(error.detail || 'Signup failed');
|
| 1180 |
+
}
|
| 1181 |
+
|
| 1182 |
+
this.showSignupModal = false;
|
| 1183 |
+
this.$buefy.toast.open({
|
| 1184 |
+
message: 'Account created successfully! Please login.',
|
| 1185 |
+
type: 'is-success'
|
| 1186 |
+
});
|
| 1187 |
+
|
| 1188 |
+
// Clear form
|
| 1189 |
+
this.signupForm = {
|
| 1190 |
+
username: '',
|
| 1191 |
+
email: '',
|
| 1192 |
+
password: '',
|
| 1193 |
+
confirmPassword: ''
|
| 1194 |
+
};
|
| 1195 |
+
|
| 1196 |
+
// Show login modal
|
| 1197 |
+
this.showLoginModal = true;
|
| 1198 |
+
} catch (error) {
|
| 1199 |
+
this.$buefy.toast.open({
|
| 1200 |
+
message: `Error: ${error.message}`,
|
| 1201 |
+
type: 'is-danger'
|
| 1202 |
+
});
|
| 1203 |
+
} finally {
|
| 1204 |
+
this.isLoading = false;
|
| 1205 |
+
}
|
| 1206 |
+
},
|
| 1207 |
+
|
| 1208 |
+
async checkAuth() {
|
| 1209 |
+
const token = localStorage.getItem('token');
|
| 1210 |
+
if (token) {
|
| 1211 |
+
try {
|
| 1212 |
+
this.token = token;
|
| 1213 |
+
const response = await fetch('/api/me', {
|
| 1214 |
+
headers: {
|
| 1215 |
+
'Authorization': `Bearer ${token}`
|
| 1216 |
+
}
|
| 1217 |
+
});
|
| 1218 |
+
|
| 1219 |
+
if (response.ok) {
|
| 1220 |
+
const userData = await response.json();
|
| 1221 |
+
this.username = userData.username;
|
| 1222 |
+
this.isAuthenticated = true;
|
| 1223 |
+
this.isAdmin = userData.is_admin;
|
| 1224 |
+
this.currentUser = userData;
|
| 1225 |
+
|
| 1226 |
+
// If user is admin, load users list
|
| 1227 |
+
if (this.isAdmin) {
|
| 1228 |
+
this.loadUsers();
|
| 1229 |
+
}
|
| 1230 |
+
} else {
|
| 1231 |
+
// Token invalid or expired
|
| 1232 |
+
this.logout();
|
| 1233 |
+
}
|
| 1234 |
+
} catch (error) {
|
| 1235 |
+
console.error('Auth check failed:', error);
|
| 1236 |
+
this.logout();
|
| 1237 |
+
}
|
| 1238 |
+
}
|
| 1239 |
+
},
|
| 1240 |
+
|
| 1241 |
+
// Add these methods to the methods section
|
| 1242 |
+
async loadUsers() {
|
| 1243 |
+
if (!this.isAdmin) return;
|
| 1244 |
+
|
| 1245 |
+
try {
|
| 1246 |
+
this.loadingUsers = true;
|
| 1247 |
+
const response = await fetch('/api/users', {
|
| 1248 |
+
headers: {
|
| 1249 |
+
'Authorization': `Bearer ${this.token}`
|
| 1250 |
+
}
|
| 1251 |
+
});
|
| 1252 |
+
|
| 1253 |
+
if (!response.ok) {
|
| 1254 |
+
throw new Error('Failed to load users');
|
| 1255 |
+
}
|
| 1256 |
+
|
| 1257 |
+
this.users = await response.json();
|
| 1258 |
+
} catch (error) {
|
| 1259 |
+
console.error('Error loading users:', error);
|
| 1260 |
+
this.$buefy.toast.open({
|
| 1261 |
+
message: `Error: ${error.message}`,
|
| 1262 |
+
type: 'is-danger'
|
| 1263 |
+
});
|
| 1264 |
+
} finally {
|
| 1265 |
+
this.loadingUsers = false;
|
| 1266 |
+
}
|
| 1267 |
+
},
|
| 1268 |
+
|
| 1269 |
+
async toggleUserStatus(user) {
|
| 1270 |
+
try {
|
| 1271 |
+
// Don't allow admins to disable themselves
|
| 1272 |
+
if (user.is_admin && user.id === this.currentUser.id && !user.disabled) {
|
| 1273 |
+
this.$buefy.toast.open({
|
| 1274 |
+
message: 'Admins cannot disable their own accounts',
|
| 1275 |
+
type: 'is-warning'
|
| 1276 |
+
});
|
| 1277 |
+
return;
|
| 1278 |
+
}
|
| 1279 |
+
|
| 1280 |
+
const action = user.disabled ? 'enable' : 'disable';
|
| 1281 |
+
const response = await fetch(`/api/users/${user.id}/${action}`, {
|
| 1282 |
+
method: 'PUT',
|
| 1283 |
+
headers: {
|
| 1284 |
+
'Authorization': `Bearer ${this.token}`,
|
| 1285 |
+
'Content-Type': 'application/json'
|
| 1286 |
+
}
|
| 1287 |
+
});
|
| 1288 |
+
|
| 1289 |
+
if (!response.ok) {
|
| 1290 |
+
const errorData = await response.json();
|
| 1291 |
+
throw new Error(errorData.detail || `Failed to ${action} user`);
|
| 1292 |
+
}
|
| 1293 |
+
|
| 1294 |
+
// Update local user data
|
| 1295 |
+
user.disabled = !user.disabled;
|
| 1296 |
+
|
| 1297 |
+
this.$buefy.toast.open({
|
| 1298 |
+
message: `User ${user.username} ${action}d successfully`,
|
| 1299 |
+
type: 'is-success'
|
| 1300 |
+
});
|
| 1301 |
+
} catch (error) {
|
| 1302 |
+
console.error(`Error ${user.disabled ? 'enabling' : 'disabling'} user:`, error);
|
| 1303 |
+
this.$buefy.toast.open({
|
| 1304 |
+
message: `Error: ${error.message}`,
|
| 1305 |
+
type: 'is-danger'
|
| 1306 |
+
});
|
| 1307 |
+
}
|
| 1308 |
+
},
|
| 1309 |
+
|
| 1310 |
+
async deleteUser(user) {
|
| 1311 |
+
try {
|
| 1312 |
+
// Don't allow admins to delete themselves
|
| 1313 |
+
if (user.id === this.currentUser.id) {
|
| 1314 |
+
this.$buefy.toast.open({
|
| 1315 |
+
message: 'You cannot delete your own account',
|
| 1316 |
+
type: 'is-warning'
|
| 1317 |
+
});
|
| 1318 |
+
return;
|
| 1319 |
+
}
|
| 1320 |
+
|
| 1321 |
+
// Show confirmation dialog
|
| 1322 |
+
this.$buefy.dialog.confirm({
|
| 1323 |
+
title: 'Delete User',
|
| 1324 |
+
message: `Are you sure you want to delete user "${user.username}"? This action cannot be undone.`,
|
| 1325 |
+
confirmText: 'Delete',
|
| 1326 |
+
type: 'is-danger',
|
| 1327 |
+
hasIcon: true,
|
| 1328 |
+
onConfirm: async () => {
|
| 1329 |
+
const response = await fetch(`/api/users/${user.id}`, {
|
| 1330 |
+
method: 'DELETE',
|
| 1331 |
+
headers: {
|
| 1332 |
+
'Authorization': `Bearer ${this.token}`
|
| 1333 |
+
}
|
| 1334 |
+
});
|
| 1335 |
+
|
| 1336 |
+
if (!response.ok) {
|
| 1337 |
+
const errorData = await response.json();
|
| 1338 |
+
throw new Error(errorData.detail || 'Failed to delete user');
|
| 1339 |
+
}
|
| 1340 |
+
|
| 1341 |
+
// Remove from local list
|
| 1342 |
+
this.users = this.users.filter(u => u.id !== user.id);
|
| 1343 |
+
|
| 1344 |
+
this.$buefy.toast.open({
|
| 1345 |
+
message: `User ${user.username} deleted successfully`,
|
| 1346 |
+
type: 'is-success'
|
| 1347 |
+
});
|
| 1348 |
+
}
|
| 1349 |
+
});
|
| 1350 |
+
} catch (error) {
|
| 1351 |
+
console.error('Error deleting user:', error);
|
| 1352 |
+
this.$buefy.toast.open({
|
| 1353 |
+
message: `Error: ${error.message}`,
|
| 1354 |
+
type: 'is-danger'
|
| 1355 |
+
});
|
| 1356 |
+
}
|
| 1357 |
+
},
|
| 1358 |
+
|
| 1359 |
+
refreshUsers() {
|
| 1360 |
+
this.loadUsers();
|
| 1361 |
+
},
|
| 1362 |
+
}, // methods
|
| 1363 |
+
// Add this to the Vue app definition, after methods
|
| 1364 |
+
computed: {
|
| 1365 |
+
filteredUsers() {
|
| 1366 |
+
if (!this.userSearchQuery) {
|
| 1367 |
+
return this.users;
|
| 1368 |
+
}
|
| 1369 |
+
|
| 1370 |
+
const query = this.userSearchQuery.toLowerCase();
|
| 1371 |
+
return this.users.filter(user =>
|
| 1372 |
+
user.username.toLowerCase().includes(query) ||
|
| 1373 |
+
user.email.toLowerCase().includes(query)
|
| 1374 |
+
);
|
| 1375 |
+
}
|
| 1376 |
+
},
|
| 1377 |
+
mounted() {
|
| 1378 |
+
this.checkAuth();
|
| 1379 |
+
this.loadTranscriptions();
|
| 1380 |
+
},
|
| 1381 |
+
watch: {
|
| 1382 |
+
audioUrl() {
|
| 1383 |
+
if (this.audioUrl) {
|
| 1384 |
+
this.initializeAudio();
|
| 1385 |
+
this.initializeWaveform();
|
| 1386 |
+
}
|
| 1387 |
+
},
|
| 1388 |
+
showAdminPanel(newVal) {
|
| 1389 |
+
if (newVal && this.isAdmin) {
|
| 1390 |
+
this.loadUsers();
|
| 1391 |
+
}
|
| 1392 |
+
}
|
| 1393 |
+
},
|
| 1394 |
+
beforeUnmount() {
|
| 1395 |
+
if (this.howl) {
|
| 1396 |
+
this.howl.unload();
|
| 1397 |
+
}
|
| 1398 |
+
this.stopSeekUpdate();
|
| 1399 |
+
}
|
| 1400 |
+
});
|
| 1401 |
+
|
| 1402 |
+
app.use(Buefy.default);
|
| 1403 |
+
app.mount('#app');
|
| 1404 |
+
</script>
|
| 1405 |
+
</body>
|
| 1406 |
+
</html>
|
main.py
ADDED
|
@@ -0,0 +1,877 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os, io, uuid, json, wave
|
| 2 |
+
import aiohttp
|
| 3 |
+
import aiofiles
|
| 4 |
+
import asyncio
|
| 5 |
+
import sqlite3
|
| 6 |
+
import bcrypt
|
| 7 |
+
import tempfile
|
| 8 |
+
from fastapi import FastAPI, Request, Query, Form, UploadFile, File, HTTPException, Depends
|
| 9 |
+
from fastapi.responses import HTMLResponse, JSONResponse
|
| 10 |
+
from fastapi.staticfiles import StaticFiles
|
| 11 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 12 |
+
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
| 13 |
+
from datetime import datetime, timedelta
|
| 14 |
+
from jose import JWTError, jwt
|
| 15 |
+
from pydantic import BaseModel, EmailStr
|
| 16 |
+
from pydantic import confloat
|
| 17 |
+
# from pydub import AudioSegment # rely on "ffmpeg" package from the system and "ffmpeg-python" library
|
| 18 |
+
from pydub.silence import split_on_silence
|
| 19 |
+
import soundfile as sf
|
| 20 |
+
import numpy as np
|
| 21 |
+
from scipy import signal
|
| 22 |
+
from scipy.io import wavfile
|
| 23 |
+
from typing import Optional, List, Dict, Union
|
| 24 |
+
from dotenv import load_dotenv
|
| 25 |
+
from sqlalchemy import create_engine, Column, Integer, String, Boolean, ForeignKey
|
| 26 |
+
from sqlalchemy.ext.declarative import declarative_base
|
| 27 |
+
from sqlalchemy.orm import sessionmaker, Session
|
| 28 |
+
|
| 29 |
+
load_dotenv()
|
| 30 |
+
|
| 31 |
+
# Constants
|
| 32 |
+
MAX_DURATION = 3600 * 2 # 2 hours
|
| 33 |
+
#CHUNK_SIZE = 1024 * 1024 # 1MB chunks
|
| 34 |
+
MAX_FILE_SIZE = 25 * 1024 * 1024 # 25MB
|
| 35 |
+
system_prompt = """
|
| 36 |
+
You are a helpful assistant. Your task is to correct
|
| 37 |
+
any spelling discrepancies in the transcribed text. Only add necessary
|
| 38 |
+
punctuation such as periods, commas, and capitalization, and use only the
|
| 39 |
+
context provided.
|
| 40 |
+
"""
|
| 41 |
+
names = []
|
| 42 |
+
extra_prompt = f"""
|
| 43 |
+
Make sure that the names of : {','.join(names)}
|
| 44 |
+
the following products are spelled correctly
|
| 45 |
+
"""
|
| 46 |
+
|
| 47 |
+
# Authentication & Security
|
| 48 |
+
SECRET_KEY = os.getenv("SECRET_KEY", None) # Change in production
|
| 49 |
+
ALGORITHM = "HS256"
|
| 50 |
+
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
| 51 |
+
|
| 52 |
+
# Database setup
|
| 53 |
+
DB_TYPE = os.getenv("DB_TYPE", "sqlite").lower() # Default to SQLite if not specified
|
| 54 |
+
DATABASE_URL = os.getenv("DATABASE_URL")
|
| 55 |
+
EMPTY_DB = os.getenv("EMPTY_DB", "false").lower() == "true" # Get EMPTY_DB flag
|
| 56 |
+
|
| 57 |
+
# Configure engine based on database type
|
| 58 |
+
if DB_TYPE == "postgresql":
|
| 59 |
+
if DATABASE_URL and DATABASE_URL.startswith("postgres://"):
|
| 60 |
+
DATABASE_URL = DATABASE_URL.replace("postgres://", "postgresql://", 1)
|
| 61 |
+
# PostgreSQL configuration for production
|
| 62 |
+
engine = create_engine(
|
| 63 |
+
DATABASE_URL,
|
| 64 |
+
pool_size=5,
|
| 65 |
+
max_overflow=10,
|
| 66 |
+
pool_timeout=30,
|
| 67 |
+
pool_recycle=1800, # Recycle connections after 30 minutes
|
| 68 |
+
)
|
| 69 |
+
elif DB_TYPE == "sqlite":
|
| 70 |
+
# SQLite configuration for development
|
| 71 |
+
engine = create_engine(
|
| 72 |
+
DATABASE_URL,
|
| 73 |
+
connect_args={"check_same_thread": False}
|
| 74 |
+
)
|
| 75 |
+
else:
|
| 76 |
+
raise ValueError("Unsupported database type. Use 'sqlite' or 'postgresql'.")
|
| 77 |
+
|
| 78 |
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
| 79 |
+
Base = declarative_base()
|
| 80 |
+
|
| 81 |
+
# SQLAlchemy Models
|
| 82 |
+
# SQLAlchemy Models
|
| 83 |
+
class UserModel(Base):
|
| 84 |
+
__tablename__ = "users"
|
| 85 |
+
|
| 86 |
+
id = Column(Integer, primary_key=True, index=True)
|
| 87 |
+
username = Column(String, unique=True, index=True)
|
| 88 |
+
email = Column(String, unique=True, index=True)
|
| 89 |
+
hashed_password = Column(String)
|
| 90 |
+
disabled = Column(Boolean, default=True) # Default to disabled
|
| 91 |
+
is_admin = Column(Boolean, default=False) # Add admin field
|
| 92 |
+
|
| 93 |
+
# Create tables
|
| 94 |
+
Base.metadata.create_all(bind=engine)
|
| 95 |
+
|
| 96 |
+
# Dependency to get DB session
|
| 97 |
+
def get_db():
|
| 98 |
+
db = SessionLocal()
|
| 99 |
+
try:
|
| 100 |
+
yield db
|
| 101 |
+
finally:
|
| 102 |
+
db.close()
|
| 103 |
+
|
| 104 |
+
# Add these models
|
| 105 |
+
class UserBase(BaseModel):
|
| 106 |
+
username: str
|
| 107 |
+
email: EmailStr
|
| 108 |
+
|
| 109 |
+
class UserCreate(UserBase):
|
| 110 |
+
password: str
|
| 111 |
+
|
| 112 |
+
class User(UserBase):
|
| 113 |
+
id: int
|
| 114 |
+
disabled: bool = True # Default to disabled
|
| 115 |
+
is_admin: bool = False # Add admin field
|
| 116 |
+
|
| 117 |
+
class Config:
|
| 118 |
+
orm_mode = True
|
| 119 |
+
|
| 120 |
+
class Token(BaseModel):
|
| 121 |
+
access_token: str
|
| 122 |
+
token_type: str
|
| 123 |
+
|
| 124 |
+
class TokenData(BaseModel):
|
| 125 |
+
username: str | None = None
|
| 126 |
+
|
| 127 |
+
# Add this with other SQLAlchemy models
|
| 128 |
+
class TranscriptionModel(Base):
|
| 129 |
+
__tablename__ = "transcriptions"
|
| 130 |
+
|
| 131 |
+
id = Column(String, primary_key=True, index=True)
|
| 132 |
+
name = Column(String)
|
| 133 |
+
text = Column(String)
|
| 134 |
+
segments = Column(String) # Store JSON as string
|
| 135 |
+
audio_file = Column(String)
|
| 136 |
+
user_id = Column(Integer, ForeignKey("users.id"))
|
| 137 |
+
created_at = Column(String, default=lambda: datetime.utcnow().isoformat())
|
| 138 |
+
|
| 139 |
+
# Add this with other Pydantic models
|
| 140 |
+
class TranscriptionBase(BaseModel):
|
| 141 |
+
name: str
|
| 142 |
+
text: str
|
| 143 |
+
segments: Optional[List[Dict[str, Union[str, float]]]] = []
|
| 144 |
+
audio_file: Optional[str] = None
|
| 145 |
+
|
| 146 |
+
class TranscriptionCreate(TranscriptionBase):
|
| 147 |
+
pass
|
| 148 |
+
|
| 149 |
+
class Transcription(TranscriptionBase):
|
| 150 |
+
id: str
|
| 151 |
+
user_id: int
|
| 152 |
+
created_at: str
|
| 153 |
+
|
| 154 |
+
class Config:
|
| 155 |
+
orm_mode = True
|
| 156 |
+
|
| 157 |
+
# Add these utilities
|
| 158 |
+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
| 159 |
+
|
| 160 |
+
# App
|
| 161 |
+
app = FastAPI()
|
| 162 |
+
|
| 163 |
+
CORS_ORIGINS = os.getenv("CORS_ORIGINS", "http://localhost:8000,http://127.0.0.1:8000").split(",")
|
| 164 |
+
|
| 165 |
+
# Configure CORS
|
| 166 |
+
app.add_middleware(
|
| 167 |
+
CORSMiddleware,
|
| 168 |
+
allow_origins=CORS_ORIGINS,
|
| 169 |
+
allow_credentials=True,
|
| 170 |
+
allow_methods=["*"],
|
| 171 |
+
allow_headers=["*"],
|
| 172 |
+
expose_headers=["*"]
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
# Create necessary directories
|
| 176 |
+
os.makedirs("uploads", exist_ok=True)
|
| 177 |
+
# os.makedirs("transcriptions", exist_ok=True) # we use DB to store
|
| 178 |
+
|
| 179 |
+
# Mount static files
|
| 180 |
+
app.mount("/static", StaticFiles(directory="static"), name="static")
|
| 181 |
+
app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads")
|
| 182 |
+
|
| 183 |
+
# Initialize database
|
| 184 |
+
def init_db():
|
| 185 |
+
try:
|
| 186 |
+
print("Creating the database...")
|
| 187 |
+
# If EMPTY_DB is True, drop all tables and recreate them
|
| 188 |
+
if EMPTY_DB:
|
| 189 |
+
print("EMPTY_DB flag is set to True. Dropping all tables...")
|
| 190 |
+
Base.metadata.drop_all(bind=engine)
|
| 191 |
+
print("All tables dropped successfully.")
|
| 192 |
+
Base.metadata.create_all(bind=engine)
|
| 193 |
+
print("Database initialized successfully")
|
| 194 |
+
except Exception as e:
|
| 195 |
+
print(f"Database: {DB_TYPE} initialization error: {str(e)}")
|
| 196 |
+
|
| 197 |
+
def verify_password(plain_password, hashed_password):
|
| 198 |
+
try:
|
| 199 |
+
# Ensure both password and hash are in correct format
|
| 200 |
+
if isinstance(hashed_password, str):
|
| 201 |
+
hashed_password = hashed_password.encode('utf-8')
|
| 202 |
+
if isinstance(plain_password, str):
|
| 203 |
+
plain_password = plain_password.encode('utf-8')
|
| 204 |
+
return bcrypt.checkpw(plain_password, hashed_password)
|
| 205 |
+
except Exception as e:
|
| 206 |
+
print(f"Password verification error: {str(e)}")
|
| 207 |
+
return False
|
| 208 |
+
|
| 209 |
+
def get_password_hash(password):
|
| 210 |
+
if isinstance(password, str):
|
| 211 |
+
password = password.encode('utf-8')
|
| 212 |
+
return bcrypt.hashpw(password, bcrypt.gensalt()).decode('utf-8')
|
| 213 |
+
|
| 214 |
+
def get_user(db: Session, username: str):
|
| 215 |
+
user = db.query(UserModel).filter(UserModel.username == username).first()
|
| 216 |
+
if user:
|
| 217 |
+
return User(
|
| 218 |
+
id=user.id,
|
| 219 |
+
username=user.username,
|
| 220 |
+
email=user.email,
|
| 221 |
+
disabled=user.disabled,
|
| 222 |
+
is_admin=user.is_admin
|
| 223 |
+
)
|
| 224 |
+
return None
|
| 225 |
+
|
| 226 |
+
def authenticate_user(db: Session, username: str, password: str):
|
| 227 |
+
user = db.query(UserModel).filter(UserModel.username == username).first()
|
| 228 |
+
if not user:
|
| 229 |
+
return False
|
| 230 |
+
if not verify_password(password, user.hashed_password):
|
| 231 |
+
return False
|
| 232 |
+
return User(
|
| 233 |
+
id=user.id,
|
| 234 |
+
username=user.username,
|
| 235 |
+
email=user.email,
|
| 236 |
+
disabled=user.disabled,
|
| 237 |
+
is_admin=user.is_admin
|
| 238 |
+
)
|
| 239 |
+
|
| 240 |
+
async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
|
| 241 |
+
credentials_exception = HTTPException(
|
| 242 |
+
status_code=401,
|
| 243 |
+
detail="Could not validate credentials",
|
| 244 |
+
headers={"WWW-Authenticate": "Bearer"},
|
| 245 |
+
)
|
| 246 |
+
try:
|
| 247 |
+
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
| 248 |
+
username: str = payload.get("sub")
|
| 249 |
+
if username is None:
|
| 250 |
+
raise credentials_exception
|
| 251 |
+
token_data = TokenData(username=username)
|
| 252 |
+
except JWTError:
|
| 253 |
+
raise credentials_exception
|
| 254 |
+
user = get_user(db, username=token_data.username)
|
| 255 |
+
if user is None:
|
| 256 |
+
raise credentials_exception
|
| 257 |
+
return user
|
| 258 |
+
|
| 259 |
+
async def get_current_active_admin(current_user: User = Depends(get_current_user)):
|
| 260 |
+
if current_user.disabled:
|
| 261 |
+
raise HTTPException(status_code=400, detail="Inactive user")
|
| 262 |
+
if not current_user.is_admin:
|
| 263 |
+
raise HTTPException(status_code=403, detail="Not enough permissions")
|
| 264 |
+
return current_user
|
| 265 |
+
|
| 266 |
+
@app.get("/api/users")
|
| 267 |
+
async def get_list_users(
|
| 268 |
+
current_user: User = Depends(get_current_active_admin),
|
| 269 |
+
db: Session = Depends(get_db)
|
| 270 |
+
):
|
| 271 |
+
users = db.query(UserModel).all()
|
| 272 |
+
return [
|
| 273 |
+
{
|
| 274 |
+
"id": user.id,
|
| 275 |
+
"username": user.username,
|
| 276 |
+
"email": user.email,
|
| 277 |
+
"disabled": user.disabled,
|
| 278 |
+
"is_admin": user.is_admin
|
| 279 |
+
}
|
| 280 |
+
for user in users
|
| 281 |
+
]
|
| 282 |
+
|
| 283 |
+
@app.put("/api/users/{user_id}/enable")
|
| 284 |
+
async def enable_user(
|
| 285 |
+
user_id: int,
|
| 286 |
+
current_user: User = Depends(get_current_active_admin),
|
| 287 |
+
db: Session = Depends(get_db)
|
| 288 |
+
):
|
| 289 |
+
user = db.query(UserModel).filter(UserModel.id == user_id).first()
|
| 290 |
+
if not user:
|
| 291 |
+
raise HTTPException(status_code=404, detail="User not found")
|
| 292 |
+
|
| 293 |
+
# Prevent disabling the last admin
|
| 294 |
+
if user.is_admin and user.id == current_user.id:
|
| 295 |
+
admin_count = db.query(UserModel).filter(UserModel.is_admin == True, UserModel.disabled == False).count()
|
| 296 |
+
if admin_count <= 1:
|
| 297 |
+
raise HTTPException(status_code=400, detail="Cannot disable the last active admin")
|
| 298 |
+
|
| 299 |
+
user.disabled = False
|
| 300 |
+
db.commit()
|
| 301 |
+
return {"message": f"User {user.username} has been enabled"}
|
| 302 |
+
|
| 303 |
+
@app.put("/api/users/{user_id}/disable")
|
| 304 |
+
async def disable_user(
|
| 305 |
+
user_id: int,
|
| 306 |
+
current_user: User = Depends(get_current_active_admin),
|
| 307 |
+
db: Session = Depends(get_db)
|
| 308 |
+
):
|
| 309 |
+
user = db.query(UserModel).filter(UserModel.id == user_id).first()
|
| 310 |
+
if not user:
|
| 311 |
+
raise HTTPException(status_code=404, detail="User not found")
|
| 312 |
+
|
| 313 |
+
# Prevent disabling yourself if you're an admin
|
| 314 |
+
if user.is_admin and user.id == current_user.id:
|
| 315 |
+
raise HTTPException(status_code=400, detail="Admins cannot disable themselves")
|
| 316 |
+
|
| 317 |
+
user.disabled = True
|
| 318 |
+
db.commit()
|
| 319 |
+
return {"message": f"User {user.username} has been disabled"}
|
| 320 |
+
|
| 321 |
+
@app.delete("/api/users/{user_id}")
|
| 322 |
+
async def delete_user(
|
| 323 |
+
user_id: int,
|
| 324 |
+
current_user: User = Depends(get_current_active_admin),
|
| 325 |
+
db: Session = Depends(get_db)
|
| 326 |
+
):
|
| 327 |
+
# Prevent deleting yourself
|
| 328 |
+
if user_id == current_user.id:
|
| 329 |
+
raise HTTPException(status_code=400, detail="Users cannot delete their own accounts")
|
| 330 |
+
|
| 331 |
+
user = db.query(UserModel).filter(UserModel.id == user_id).first()
|
| 332 |
+
if not user:
|
| 333 |
+
raise HTTPException(status_code=404, detail="User not found")
|
| 334 |
+
|
| 335 |
+
# Get username before deletion for the response
|
| 336 |
+
username = user.username
|
| 337 |
+
|
| 338 |
+
# Delete the user
|
| 339 |
+
db.delete(user)
|
| 340 |
+
db.commit()
|
| 341 |
+
|
| 342 |
+
return {"message": f"User {username} has been deleted"}
|
| 343 |
+
|
| 344 |
+
def create_access_token(data: dict, expires_delta: timedelta | None = None):
|
| 345 |
+
to_encode = data.copy()
|
| 346 |
+
if expires_delta:
|
| 347 |
+
expire = datetime.utcnow() + expires_delta
|
| 348 |
+
else:
|
| 349 |
+
expire = datetime.utcnow() + timedelta(minutes=15)
|
| 350 |
+
to_encode.update({"exp": expire})
|
| 351 |
+
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
| 352 |
+
return encoded_jwt
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
### FUNCTIONS
|
| 356 |
+
|
| 357 |
+
def convert_to_flac(input_file: str, output_file: str):
|
| 358 |
+
"""
|
| 359 |
+
Convert audio to 16KHz mono FLAC format.
|
| 360 |
+
"""
|
| 361 |
+
try:
|
| 362 |
+
# Read the audio file
|
| 363 |
+
data, samplerate = sf.read(input_file)
|
| 364 |
+
|
| 365 |
+
# Convert to mono if stereo
|
| 366 |
+
if len(data.shape) > 1:
|
| 367 |
+
data = np.mean(data, axis=1)
|
| 368 |
+
|
| 369 |
+
# Resample to 16KHz if needed
|
| 370 |
+
if samplerate != 16000:
|
| 371 |
+
ratio = 16000 / samplerate
|
| 372 |
+
data = signal.resample(data, int(len(data) * ratio))
|
| 373 |
+
samplerate = 16000
|
| 374 |
+
|
| 375 |
+
# Save as FLAC
|
| 376 |
+
sf.write(output_file, data, samplerate, format='FLAC')
|
| 377 |
+
|
| 378 |
+
except Exception as e:
|
| 379 |
+
raise HTTPException(500, f"Error converting to FLAC: {str(e)}")
|
| 380 |
+
|
| 381 |
+
######## ROUTES
|
| 382 |
+
|
| 383 |
+
# Near the top where CORS is configured
|
| 384 |
+
app.add_middleware(
|
| 385 |
+
CORSMiddleware,
|
| 386 |
+
allow_origins=["*"], # For production, replace with specific domains
|
| 387 |
+
allow_credentials=True,
|
| 388 |
+
allow_methods=["*"],
|
| 389 |
+
allow_headers=["*"],
|
| 390 |
+
expose_headers=["*"]
|
| 391 |
+
)
|
| 392 |
+
|
| 393 |
+
# Also ensure your login form data is properly sent
|
| 394 |
+
@app.post("/token")
|
| 395 |
+
async def login_for_access_token(
|
| 396 |
+
form_data: OAuth2PasswordRequestForm = Depends(),
|
| 397 |
+
db: Session = Depends(get_db)
|
| 398 |
+
):
|
| 399 |
+
try:
|
| 400 |
+
user = authenticate_user(db, form_data.username, form_data.password)
|
| 401 |
+
if not user:
|
| 402 |
+
raise HTTPException(
|
| 403 |
+
status_code=401,
|
| 404 |
+
detail="Incorrect username or password",
|
| 405 |
+
headers={"WWW-Authenticate": "Bearer"},
|
| 406 |
+
)
|
| 407 |
+
if user.disabled:
|
| 408 |
+
raise HTTPException(
|
| 409 |
+
status_code=400,
|
| 410 |
+
detail="User is disabled"
|
| 411 |
+
)
|
| 412 |
+
access_token = create_access_token(data={"sub": user.username})
|
| 413 |
+
return {"access_token": access_token, "token_type": "bearer"}
|
| 414 |
+
except Exception as e:
|
| 415 |
+
print(f"Login error: {str(e)}") # Add logging for debugging
|
| 416 |
+
raise HTTPException(
|
| 417 |
+
status_code=500,
|
| 418 |
+
detail=f"Internal server error: {str(e)}"
|
| 419 |
+
)
|
| 420 |
+
|
| 421 |
+
# @app.post("/token", response_model=Token)
|
| 422 |
+
# async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
|
| 423 |
+
# user = authenticate_user(db, form_data.username, form_data.password)
|
| 424 |
+
# if not user:
|
| 425 |
+
# raise HTTPException(
|
| 426 |
+
# status_code=401,
|
| 427 |
+
# detail="Incorrect username or password",
|
| 428 |
+
# headers={"WWW-Authenticate": "Bearer"},
|
| 429 |
+
# )
|
| 430 |
+
# access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
| 431 |
+
# access_token = create_access_token(
|
| 432 |
+
# data={"sub": user.username}, expires_delta=access_token_expires
|
| 433 |
+
# )
|
| 434 |
+
# return {"access_token": access_token, "token_type": "bearer"}
|
| 435 |
+
|
| 436 |
+
@app.post("/api/signup")
|
| 437 |
+
async def signup(user: UserCreate, db: Session = Depends(get_db)):
|
| 438 |
+
try:
|
| 439 |
+
# Check if username or email already exists
|
| 440 |
+
db_user = db.query(UserModel).filter(
|
| 441 |
+
(UserModel.username == user.username) | (UserModel.email == user.email)
|
| 442 |
+
).first()
|
| 443 |
+
|
| 444 |
+
if db_user:
|
| 445 |
+
raise HTTPException(
|
| 446 |
+
status_code=400,
|
| 447 |
+
detail="Username or email already exists"
|
| 448 |
+
)
|
| 449 |
+
|
| 450 |
+
# Check if this is the first user
|
| 451 |
+
user_count = db.query(UserModel).count()
|
| 452 |
+
is_first_user = user_count == 0
|
| 453 |
+
|
| 454 |
+
# Create new user
|
| 455 |
+
hashed_password = get_password_hash(user.password)
|
| 456 |
+
db_user = UserModel(
|
| 457 |
+
username=user.username,
|
| 458 |
+
email=user.email,
|
| 459 |
+
hashed_password=hashed_password,
|
| 460 |
+
disabled=not is_first_user, # First user is enabled, others disabled
|
| 461 |
+
is_admin=is_first_user # First user is admin
|
| 462 |
+
)
|
| 463 |
+
db.add(db_user)
|
| 464 |
+
db.commit()
|
| 465 |
+
db.refresh(db_user)
|
| 466 |
+
|
| 467 |
+
return {"message": "User created successfully", "is_admin": is_first_user, "disabled": not is_first_user}
|
| 468 |
+
except HTTPException:
|
| 469 |
+
raise
|
| 470 |
+
except Exception as e:
|
| 471 |
+
db.rollback()
|
| 472 |
+
raise HTTPException(
|
| 473 |
+
status_code=500,
|
| 474 |
+
detail=f"An error occurred: {str(e)}"
|
| 475 |
+
)
|
| 476 |
+
|
| 477 |
+
# Modify the existing routes to require authentication
|
| 478 |
+
@app.get("/api/me", response_model=User)
|
| 479 |
+
async def read_users_me(current_user: User = Depends(get_current_user)):
|
| 480 |
+
return current_user
|
| 481 |
+
|
| 482 |
+
@app.get("/", response_class=HTMLResponse)
|
| 483 |
+
async def read_index():
|
| 484 |
+
async with aiofiles.open("index.html", mode="r") as f:
|
| 485 |
+
content = await f.read()
|
| 486 |
+
return HTMLResponse(content=content)
|
| 487 |
+
|
| 488 |
+
# Helper function to split audio into chunks
|
| 489 |
+
def split_audio_chunks(
|
| 490 |
+
file_path: str,
|
| 491 |
+
chunk_size: int = 600, # 10 minutes in seconds
|
| 492 |
+
overlap: int = 5 # 5 seconds overlap
|
| 493 |
+
) -> List[Dict[str, Union[str, float]]]:
|
| 494 |
+
"""
|
| 495 |
+
Split audio into chunks with overlap and save as FLAC files.
|
| 496 |
+
"""
|
| 497 |
+
try:
|
| 498 |
+
# Check file size
|
| 499 |
+
file_size = os.path.getsize(file_path)
|
| 500 |
+
if file_size > MAX_FILE_SIZE:
|
| 501 |
+
print(f"Warning: File size ({file_size / (1024 * 1024):.2f}MB) exceeds maximum size limit ({MAX_FILE_SIZE / (1024 * 1024)}MB)")
|
| 502 |
+
else:
|
| 503 |
+
print(f"File size: {file_size / (1024 * 1024):.2f}MB")
|
| 504 |
+
# Check duration
|
| 505 |
+
# with wave.open(file_path, 'rb') as wav_file:
|
| 506 |
+
# duration = wav_file.getnframes() / wav_file.getframerate()
|
| 507 |
+
# if duration > MAX_DURATION:
|
| 508 |
+
# print(f"Warning: File duration ({duration:.2f}s) exceeds maximum duration limit ({MAX_DURATION:.2f}s)")
|
| 509 |
+
# else:
|
| 510 |
+
# print(f"File duration: {duration:.2f}s")
|
| 511 |
+
|
| 512 |
+
# Read audio file using soundfile (supports multiple formats)
|
| 513 |
+
audio_data, sample_rate = sf.read(file_path)
|
| 514 |
+
|
| 515 |
+
# Convert stereo to mono if needed
|
| 516 |
+
if len(audio_data.shape) > 1:
|
| 517 |
+
audio_data = np.mean(audio_data, axis=1)
|
| 518 |
+
|
| 519 |
+
# Calculate chunk size in samples
|
| 520 |
+
chunk_size_samples = chunk_size * sample_rate
|
| 521 |
+
overlap_samples = overlap * sample_rate
|
| 522 |
+
|
| 523 |
+
chunks = []
|
| 524 |
+
start = 0
|
| 525 |
+
|
| 526 |
+
while start < len(audio_data):
|
| 527 |
+
end = min(start + chunk_size_samples, len(audio_data))
|
| 528 |
+
chunk_data = audio_data[start:end]
|
| 529 |
+
|
| 530 |
+
# Save chunk as FLAC
|
| 531 |
+
chunk_path = f"{file_path}_chunk_{len(chunks)}.flac"
|
| 532 |
+
sf.write(chunk_path, chunk_data, sample_rate, format='FLAC')
|
| 533 |
+
|
| 534 |
+
chunks.append({
|
| 535 |
+
"path": chunk_path,
|
| 536 |
+
"start": start / sample_rate, # Start time in seconds
|
| 537 |
+
"end": end / sample_rate # End time in seconds
|
| 538 |
+
})
|
| 539 |
+
|
| 540 |
+
# Move start position with overlap
|
| 541 |
+
start += chunk_size_samples - overlap_samples
|
| 542 |
+
|
| 543 |
+
return chunks
|
| 544 |
+
|
| 545 |
+
except Exception as e:
|
| 546 |
+
raise HTTPException(500, f"Error splitting audio: {str(e)}")
|
| 547 |
+
|
| 548 |
+
# Process individual chunk
|
| 549 |
+
async def process_chunk(
|
| 550 |
+
chunk_path: str,
|
| 551 |
+
model: str,
|
| 552 |
+
language: str,
|
| 553 |
+
temperature: float,
|
| 554 |
+
response_format: str,
|
| 555 |
+
prompt: str,
|
| 556 |
+
chunk_offset: float,
|
| 557 |
+
semaphore: asyncio.Semaphore
|
| 558 |
+
):
|
| 559 |
+
"""
|
| 560 |
+
Process a single audio chunk with the transcription API.
|
| 561 |
+
"""
|
| 562 |
+
async with semaphore:
|
| 563 |
+
try:
|
| 564 |
+
async with aiofiles.open(chunk_path, "rb") as f:
|
| 565 |
+
file_data = await f.read()
|
| 566 |
+
|
| 567 |
+
form_data = aiohttp.FormData()
|
| 568 |
+
form_data.add_field('file', file_data, filename=chunk_path)
|
| 569 |
+
form_data.add_field('model', model)
|
| 570 |
+
form_data.add_field('temperature', str(temperature))
|
| 571 |
+
form_data.add_field('response_format', response_format)
|
| 572 |
+
if language != "auto":
|
| 573 |
+
form_data.add_field('language', language)
|
| 574 |
+
if prompt:
|
| 575 |
+
form_data.add_field('prompt', prompt)
|
| 576 |
+
|
| 577 |
+
print(f"Processing chunk {chunk_path}... prompt: {prompt}")
|
| 578 |
+
async with aiohttp.ClientSession() as session:
|
| 579 |
+
async with session.post(
|
| 580 |
+
f"{os.getenv('OPENAI_BASE_URL')}/audio/transcriptions",
|
| 581 |
+
headers={'Authorization': f"Bearer {os.getenv('OPENAI_API_KEY')}"},
|
| 582 |
+
data=form_data
|
| 583 |
+
) as response:
|
| 584 |
+
result = await response.json() if response_format != "text" else await response.text()
|
| 585 |
+
|
| 586 |
+
# Adjust timestamps for chunks
|
| 587 |
+
if response_format == "verbose_json":
|
| 588 |
+
for segment in result.get("segments", []):
|
| 589 |
+
segment["start"] += chunk_offset
|
| 590 |
+
segment["end"] += chunk_offset
|
| 591 |
+
return result
|
| 592 |
+
|
| 593 |
+
except Exception as e:
|
| 594 |
+
# Add detailed error logging
|
| 595 |
+
print(f"Error processing chunk {chunk_path}:")
|
| 596 |
+
print(f"Type: {type(e).__name__}")
|
| 597 |
+
print(f"Message: {str(e)}")
|
| 598 |
+
return {"error": str(e), "chunk": chunk_path}
|
| 599 |
+
|
| 600 |
+
# Combine results from all chunks
|
| 601 |
+
def combine_results(results: list, response_format: str):
|
| 602 |
+
"""Combine results from all chunks with validation"""
|
| 603 |
+
final = {"text": "", "segments": []}
|
| 604 |
+
error_chunks = []
|
| 605 |
+
|
| 606 |
+
for idx, result in enumerate(results):
|
| 607 |
+
if "error" in result:
|
| 608 |
+
error_chunks.append({
|
| 609 |
+
"chunk": idx,
|
| 610 |
+
"error": result["error"]
|
| 611 |
+
})
|
| 612 |
+
continue
|
| 613 |
+
|
| 614 |
+
# Validate response structure
|
| 615 |
+
if response_format == "verbose_json":
|
| 616 |
+
if not isinstance(result.get("segments"), list):
|
| 617 |
+
error_chunks.append({
|
| 618 |
+
"chunk": idx,
|
| 619 |
+
"error": "Invalid segments format"
|
| 620 |
+
})
|
| 621 |
+
continue
|
| 622 |
+
|
| 623 |
+
# Validate segment timestamps
|
| 624 |
+
for segment in result.get("segments", []):
|
| 625 |
+
if not all(key in segment for key in ['start', 'end', 'text']):
|
| 626 |
+
error_chunks.append({
|
| 627 |
+
"chunk": idx,
|
| 628 |
+
"error": f"Invalid segment format in chunk {idx}"
|
| 629 |
+
})
|
| 630 |
+
break
|
| 631 |
+
|
| 632 |
+
final["segments"].extend(result.get("segments", []))
|
| 633 |
+
|
| 634 |
+
# Add debug information to response
|
| 635 |
+
final["metadata"] = {
|
| 636 |
+
"total_chunks": len(results),
|
| 637 |
+
"failed_chunks": len(error_chunks),
|
| 638 |
+
"errors": error_chunks
|
| 639 |
+
}
|
| 640 |
+
|
| 641 |
+
return final
|
| 642 |
+
|
| 643 |
+
@app.post("/api/upload")
|
| 644 |
+
async def create_upload_file(
|
| 645 |
+
current_user: User = Depends(get_current_user),
|
| 646 |
+
file: UploadFile = File(...),
|
| 647 |
+
model: str = Form(default="whisper-large-v3-turbo"),
|
| 648 |
+
language: str = Form(default="auto"),
|
| 649 |
+
temperature: float = Form(default=0.0),
|
| 650 |
+
response_format: str = Form(default="verbose_json"),
|
| 651 |
+
prompt: str = Form(default=""),
|
| 652 |
+
chunk_size: int = Form(default=600),
|
| 653 |
+
overlap: int = Form(default=5),
|
| 654 |
+
start_time: float = Form(default=0.0),
|
| 655 |
+
end_time: float = Form(default=None),
|
| 656 |
+
db: Session = Depends(get_db)
|
| 657 |
+
):
|
| 658 |
+
try:
|
| 659 |
+
# Validate input
|
| 660 |
+
if not file.content_type.startswith('audio/'):
|
| 661 |
+
raise HTTPException(400, "Only audio files allowed")
|
| 662 |
+
|
| 663 |
+
# Save original file
|
| 664 |
+
temp_dir = tempfile.gettempdir()
|
| 665 |
+
# os.makedirs(temp_dir, exist_ok=True)
|
| 666 |
+
temp_path = os.path.join(temp_dir, file.filename)
|
| 667 |
+
temp_path = f"{temp_dir}/{file.filename}"
|
| 668 |
+
|
| 669 |
+
async with aiofiles.open(temp_path, "wb") as f:
|
| 670 |
+
await f.write(await file.read())
|
| 671 |
+
|
| 672 |
+
# Read the audio file
|
| 673 |
+
audio_data, sample_rate = sf.read(temp_path)
|
| 674 |
+
|
| 675 |
+
# Convert stereo to mono if needed
|
| 676 |
+
if len(audio_data.shape) > 1:
|
| 677 |
+
audio_data = np.mean(audio_data, axis=1)
|
| 678 |
+
|
| 679 |
+
# Calculate time boundaries in samples
|
| 680 |
+
start_sample = int(start_time * sample_rate)
|
| 681 |
+
end_sample = int(end_time * sample_rate) if end_time else len(audio_data)
|
| 682 |
+
|
| 683 |
+
# Validate boundaries
|
| 684 |
+
if start_sample >= len(audio_data):
|
| 685 |
+
raise HTTPException(400, "Start time exceeds audio duration")
|
| 686 |
+
if end_sample > len(audio_data):
|
| 687 |
+
end_sample = len(audio_data)
|
| 688 |
+
if start_sample >= end_sample:
|
| 689 |
+
raise HTTPException(400, "Invalid time range")
|
| 690 |
+
|
| 691 |
+
# Slice the audio data
|
| 692 |
+
audio_data = audio_data[start_sample:end_sample]
|
| 693 |
+
|
| 694 |
+
# Save the sliced audio
|
| 695 |
+
sliced_path = f"{temp_path}_sliced.flac"
|
| 696 |
+
sf.write(sliced_path, audio_data, sample_rate, format='FLAC')
|
| 697 |
+
|
| 698 |
+
# Split into FLAC chunks
|
| 699 |
+
chunks = split_audio_chunks(
|
| 700 |
+
sliced_path,
|
| 701 |
+
chunk_size=chunk_size,
|
| 702 |
+
overlap=overlap
|
| 703 |
+
)
|
| 704 |
+
|
| 705 |
+
# Process chunks concurrently
|
| 706 |
+
semaphore = asyncio.Semaphore(3)
|
| 707 |
+
tasks = [process_chunk(
|
| 708 |
+
chunk_path=chunk["path"],
|
| 709 |
+
model=model,
|
| 710 |
+
language=language,
|
| 711 |
+
temperature=temperature,
|
| 712 |
+
response_format=response_format,
|
| 713 |
+
prompt=prompt,
|
| 714 |
+
chunk_offset=chunk["start"] + start_time, # Adjust offset to include start_time
|
| 715 |
+
semaphore=semaphore
|
| 716 |
+
) for chunk in chunks]
|
| 717 |
+
|
| 718 |
+
results = await asyncio.gather(*tasks)
|
| 719 |
+
|
| 720 |
+
# Combine results
|
| 721 |
+
combined = combine_results(results, response_format)
|
| 722 |
+
|
| 723 |
+
# Cleanup
|
| 724 |
+
for chunk in chunks:
|
| 725 |
+
os.remove(chunk["path"])
|
| 726 |
+
os.remove(temp_path)
|
| 727 |
+
os.remove(sliced_path)
|
| 728 |
+
|
| 729 |
+
return combined
|
| 730 |
+
|
| 731 |
+
except Exception as e:
|
| 732 |
+
# Cleanup in case of error
|
| 733 |
+
if 'temp_path' in locals() and os.path.exists(temp_path):
|
| 734 |
+
os.remove(temp_path)
|
| 735 |
+
if 'sliced_path' in locals() and os.path.exists(sliced_path):
|
| 736 |
+
os.remove(sliced_path)
|
| 737 |
+
raise HTTPException(500, str(e))
|
| 738 |
+
|
| 739 |
+
@app.post("/api/upload-audio")
|
| 740 |
+
async def upload_audio(
|
| 741 |
+
request: Request,
|
| 742 |
+
current_user: User = Depends(get_current_user),
|
| 743 |
+
db: Session = Depends(get_db)
|
| 744 |
+
):
|
| 745 |
+
try:
|
| 746 |
+
form_data = await request.form()
|
| 747 |
+
file = form_data["file"]
|
| 748 |
+
|
| 749 |
+
file_id = str(uuid.uuid4())
|
| 750 |
+
filename = f"{file_id}_{file.filename}"
|
| 751 |
+
file_path = os.path.join("uploads", filename)
|
| 752 |
+
print(f"file_path: ${file_path}")
|
| 753 |
+
|
| 754 |
+
contents = await file.read()
|
| 755 |
+
async with aiofiles.open(file_path, "wb") as f:
|
| 756 |
+
await f.write(contents)
|
| 757 |
+
|
| 758 |
+
return JSONResponse({"filename": filename})
|
| 759 |
+
except Exception as e:
|
| 760 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 761 |
+
|
| 762 |
+
@app.get("/api/transcriptions")
|
| 763 |
+
async def get_transcriptions(
|
| 764 |
+
current_user: User = Depends(get_current_user),
|
| 765 |
+
db: Session = Depends(get_db)
|
| 766 |
+
):
|
| 767 |
+
try:
|
| 768 |
+
# Query transcriptions from database instead of reading files
|
| 769 |
+
transcriptions = db.query(TranscriptionModel).filter(
|
| 770 |
+
TranscriptionModel.user_id == current_user.id
|
| 771 |
+
).all()
|
| 772 |
+
|
| 773 |
+
return [
|
| 774 |
+
{
|
| 775 |
+
"id": t.id,
|
| 776 |
+
"name": t.name,
|
| 777 |
+
"text": t.text,
|
| 778 |
+
"segments": json.loads(t.segments) if t.segments else [],
|
| 779 |
+
"audio_file": t.audio_file
|
| 780 |
+
}
|
| 781 |
+
for t in transcriptions
|
| 782 |
+
]
|
| 783 |
+
except Exception as e:
|
| 784 |
+
print(f"Error getting transcriptions: {str(e)}")
|
| 785 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 786 |
+
|
| 787 |
+
@app.post("/api/save-transcription")
|
| 788 |
+
async def save_transcription(
|
| 789 |
+
data: dict,
|
| 790 |
+
current_user: User = Depends(get_current_user),
|
| 791 |
+
db: Session = Depends(get_db)
|
| 792 |
+
):
|
| 793 |
+
try:
|
| 794 |
+
trans_id = str(uuid.uuid4())
|
| 795 |
+
|
| 796 |
+
# Create new transcription in database
|
| 797 |
+
transcription = TranscriptionModel(
|
| 798 |
+
id=trans_id,
|
| 799 |
+
name=f"t{trans_id[:8]}",
|
| 800 |
+
text=data["text"],
|
| 801 |
+
segments=json.dumps(data.get("segments", [])),
|
| 802 |
+
audio_file=data.get("audio_file"),
|
| 803 |
+
user_id=current_user.id
|
| 804 |
+
)
|
| 805 |
+
|
| 806 |
+
db.add(transcription)
|
| 807 |
+
db.commit()
|
| 808 |
+
|
| 809 |
+
return {"status": "success", "id": trans_id}
|
| 810 |
+
except Exception as e:
|
| 811 |
+
db.rollback()
|
| 812 |
+
print(f"Error saving transcription: {str(e)}")
|
| 813 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 814 |
+
|
| 815 |
+
@app.get("/api/transcriptions/{trans_id}")
|
| 816 |
+
async def get_transcription(
|
| 817 |
+
trans_id: str,
|
| 818 |
+
current_user: User = Depends(get_current_user),
|
| 819 |
+
db: Session = Depends(get_db)
|
| 820 |
+
):
|
| 821 |
+
try:
|
| 822 |
+
# Query from database instead of reading file
|
| 823 |
+
transcription = db.query(TranscriptionModel).filter(
|
| 824 |
+
TranscriptionModel.id == trans_id,
|
| 825 |
+
TranscriptionModel.user_id == current_user.id
|
| 826 |
+
).first()
|
| 827 |
+
|
| 828 |
+
if not transcription:
|
| 829 |
+
raise HTTPException(status_code=404, detail="Transcription not found")
|
| 830 |
+
|
| 831 |
+
return {
|
| 832 |
+
"id": transcription.id,
|
| 833 |
+
"name": transcription.name,
|
| 834 |
+
"text": transcription.text,
|
| 835 |
+
"segments": json.loads(transcription.segments) if transcription.segments else [],
|
| 836 |
+
"audio_file": transcription.audio_file
|
| 837 |
+
}
|
| 838 |
+
except HTTPException:
|
| 839 |
+
raise
|
| 840 |
+
except Exception as e:
|
| 841 |
+
print(f"Error getting transcription: {str(e)}")
|
| 842 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 843 |
+
|
| 844 |
+
@app.delete("/api/transcriptions/{trans_id}")
|
| 845 |
+
async def delete_transcription(
|
| 846 |
+
trans_id: str,
|
| 847 |
+
current_user: User = Depends(get_current_user),
|
| 848 |
+
db: Session = Depends(get_db)
|
| 849 |
+
):
|
| 850 |
+
try:
|
| 851 |
+
# Query and delete from database
|
| 852 |
+
transcription = db.query(TranscriptionModel).filter(
|
| 853 |
+
TranscriptionModel.id == trans_id,
|
| 854 |
+
TranscriptionModel.user_id == current_user.id
|
| 855 |
+
).first()
|
| 856 |
+
|
| 857 |
+
if not transcription:
|
| 858 |
+
raise HTTPException(status_code=404, detail="Transcription not found")
|
| 859 |
+
|
| 860 |
+
db.delete(transcription)
|
| 861 |
+
db.commit()
|
| 862 |
+
|
| 863 |
+
return {"status": "success", "message": "Transcription deleted successfully"}
|
| 864 |
+
except HTTPException:
|
| 865 |
+
raise
|
| 866 |
+
except Exception as e:
|
| 867 |
+
db.rollback()
|
| 868 |
+
print(f"Error deleting transcription: {str(e)}")
|
| 869 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 870 |
+
|
| 871 |
+
@app.on_event("startup")
|
| 872 |
+
async def startup_event():
|
| 873 |
+
init_db()
|
| 874 |
+
|
| 875 |
+
if __name__ == "__main__":
|
| 876 |
+
import uvicorn
|
| 877 |
+
uvicorn.run(app, host="127.0.0.1", port=8000)
|
requirements.txt
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
python-multipart==0.0.9
|
| 2 |
fastapi==0.111.0
|
| 3 |
uvicorn==0.29.0
|
|
|
|
| 4 |
typing_extensions==4.11.0
|
| 5 |
pydantic==2.7.1
|
| 6 |
aiohttp==3.11.11
|
|
|
|
| 1 |
python-multipart==0.0.9
|
| 2 |
fastapi==0.111.0
|
| 3 |
uvicorn==0.29.0
|
| 4 |
+
gunicorn>=20.1.0
|
| 5 |
typing_extensions==4.11.0
|
| 6 |
pydantic==2.7.1
|
| 7 |
aiohttp==3.11.11
|
static/.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
static/css/buefy.min.css
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
static/css/materialdesignicons.css.map
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
static/css/materialdesignicons.min.css
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
static/favicon.ico
ADDED
|
|
static/fonts/materialdesignicons-webfont.ttf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:61e8aba5a4e981fe22cf7c8e8bcdbea00476e75c62c37f01bf7ee33361d68428
|
| 3 |
+
size 1307660
|
static/fonts/materialdesignicons-webfont.woff
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a5928a0d5c2f624e46f98d9b15c2f60045377f7c594dd78a1759132ea3b463eb
|
| 3 |
+
size 587984
|
static/fonts/materialdesignicons-webfont.woff2
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:662fefa8f2f8a95c18588d21774789c107c64e771cbe65a69af46291c4311afc
|
| 3 |
+
size 403216
|
static/js/buefy.min.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
static/js/howler.min.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*! howler.js v2.2.4 | (c) 2013-2020, James Simpson of GoldFire Studios | MIT License | howlerjs.com */
|
| 2 |
+
!function(){"use strict";var e=function(){this.init()};e.prototype={init:function(){var e=this||n;return e._counter=1e3,e._html5AudioPool=[],e.html5PoolSize=10,e._codecs={},e._howls=[],e._muted=!1,e._volume=1,e._canPlayEvent="canplaythrough",e._navigator="undefined"!=typeof window&&window.navigator?window.navigator:null,e.masterGain=null,e.noAudio=!1,e.usingWebAudio=!0,e.autoSuspend=!0,e.ctx=null,e.autoUnlock=!0,e._setup(),e},volume:function(e){var o=this||n;if(e=parseFloat(e),o.ctx||_(),void 0!==e&&e>=0&&e<=1){if(o._volume=e,o._muted)return o;o.usingWebAudio&&o.masterGain.gain.setValueAtTime(e,n.ctx.currentTime);for(var t=0;t<o._howls.length;t++)if(!o._howls[t]._webAudio)for(var r=o._howls[t]._getSoundIds(),a=0;a<r.length;a++){var u=o._howls[t]._soundById(r[a]);u&&u._node&&(u._node.volume=u._volume*e)}return o}return o._volume},mute:function(e){var o=this||n;o.ctx||_(),o._muted=e,o.usingWebAudio&&o.masterGain.gain.setValueAtTime(e?0:o._volume,n.ctx.currentTime);for(var t=0;t<o._howls.length;t++)if(!o._howls[t]._webAudio)for(var r=o._howls[t]._getSoundIds(),a=0;a<r.length;a++){var u=o._howls[t]._soundById(r[a]);u&&u._node&&(u._node.muted=!!e||u._muted)}return o},stop:function(){for(var e=this||n,o=0;o<e._howls.length;o++)e._howls[o].stop();return e},unload:function(){for(var e=this||n,o=e._howls.length-1;o>=0;o--)e._howls[o].unload();return e.usingWebAudio&&e.ctx&&void 0!==e.ctx.close&&(e.ctx.close(),e.ctx=null,_()),e},codecs:function(e){return(this||n)._codecs[e.replace(/^x-/,"")]},_setup:function(){var e=this||n;if(e.state=e.ctx?e.ctx.state||"suspended":"suspended",e._autoSuspend(),!e.usingWebAudio)if("undefined"!=typeof Audio)try{var o=new Audio;void 0===o.oncanplaythrough&&(e._canPlayEvent="canplay")}catch(n){e.noAudio=!0}else e.noAudio=!0;try{var o=new Audio;o.muted&&(e.noAudio=!0)}catch(e){}return e.noAudio||e._setupCodecs(),e},_setupCodecs:function(){var e=this||n,o=null;try{o="undefined"!=typeof Audio?new Audio:null}catch(n){return e}if(!o||"function"!=typeof o.canPlayType)return e;var t=o.canPlayType("audio/mpeg;").replace(/^no$/,""),r=e._navigator?e._navigator.userAgent:"",a=r.match(/OPR\/(\d+)/g),u=a&&parseInt(a[0].split("/")[1],10)<33,d=-1!==r.indexOf("Safari")&&-1===r.indexOf("Chrome"),i=r.match(/Version\/(.*?) /),_=d&&i&&parseInt(i[1],10)<15;return e._codecs={mp3:!(u||!t&&!o.canPlayType("audio/mp3;").replace(/^no$/,"")),mpeg:!!t,opus:!!o.canPlayType('audio/ogg; codecs="opus"').replace(/^no$/,""),ogg:!!o.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),oga:!!o.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),wav:!!(o.canPlayType('audio/wav; codecs="1"')||o.canPlayType("audio/wav")).replace(/^no$/,""),aac:!!o.canPlayType("audio/aac;").replace(/^no$/,""),caf:!!o.canPlayType("audio/x-caf;").replace(/^no$/,""),m4a:!!(o.canPlayType("audio/x-m4a;")||o.canPlayType("audio/m4a;")||o.canPlayType("audio/aac;")).replace(/^no$/,""),m4b:!!(o.canPlayType("audio/x-m4b;")||o.canPlayType("audio/m4b;")||o.canPlayType("audio/aac;")).replace(/^no$/,""),mp4:!!(o.canPlayType("audio/x-mp4;")||o.canPlayType("audio/mp4;")||o.canPlayType("audio/aac;")).replace(/^no$/,""),weba:!(_||!o.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/,"")),webm:!(_||!o.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/,"")),dolby:!!o.canPlayType('audio/mp4; codecs="ec-3"').replace(/^no$/,""),flac:!!(o.canPlayType("audio/x-flac;")||o.canPlayType("audio/flac;")).replace(/^no$/,"")},e},_unlockAudio:function(){var e=this||n;if(!e._audioUnlocked&&e.ctx){e._audioUnlocked=!1,e.autoUnlock=!1,e._mobileUnloaded||44100===e.ctx.sampleRate||(e._mobileUnloaded=!0,e.unload()),e._scratchBuffer=e.ctx.createBuffer(1,1,22050);var o=function(n){for(;e._html5AudioPool.length<e.html5PoolSize;)try{var t=new Audio;t._unlocked=!0,e._releaseHtml5Audio(t)}catch(n){e.noAudio=!0;break}for(var r=0;r<e._howls.length;r++)if(!e._howls[r]._webAudio)for(var a=e._howls[r]._getSoundIds(),u=0;u<a.length;u++){var d=e._howls[r]._soundById(a[u]);d&&d._node&&!d._node._unlocked&&(d._node._unlocked=!0,d._node.load())}e._autoResume();var i=e.ctx.createBufferSource();i.buffer=e._scratchBuffer,i.connect(e.ctx.destination),void 0===i.start?i.noteOn(0):i.start(0),"function"==typeof e.ctx.resume&&e.ctx.resume(),i.onended=function(){i.disconnect(0),e._audioUnlocked=!0,document.removeEventListener("touchstart",o,!0),document.removeEventListener("touchend",o,!0),document.removeEventListener("click",o,!0),document.removeEventListener("keydown",o,!0);for(var n=0;n<e._howls.length;n++)e._howls[n]._emit("unlock")}};return document.addEventListener("touchstart",o,!0),document.addEventListener("touchend",o,!0),document.addEventListener("click",o,!0),document.addEventListener("keydown",o,!0),e}},_obtainHtml5Audio:function(){var e=this||n;if(e._html5AudioPool.length)return e._html5AudioPool.pop();var o=(new Audio).play();return o&&"undefined"!=typeof Promise&&(o instanceof Promise||"function"==typeof o.then)&&o.catch(function(){console.warn("HTML5 Audio pool exhausted, returning potentially locked audio object.")}),new Audio},_releaseHtml5Audio:function(e){var o=this||n;return e._unlocked&&o._html5AudioPool.push(e),o},_autoSuspend:function(){var e=this;if(e.autoSuspend&&e.ctx&&void 0!==e.ctx.suspend&&n.usingWebAudio){for(var o=0;o<e._howls.length;o++)if(e._howls[o]._webAudio)for(var t=0;t<e._howls[o]._sounds.length;t++)if(!e._howls[o]._sounds[t]._paused)return e;return e._suspendTimer&&clearTimeout(e._suspendTimer),e._suspendTimer=setTimeout(function(){if(e.autoSuspend){e._suspendTimer=null,e.state="suspending";var n=function(){e.state="suspended",e._resumeAfterSuspend&&(delete e._resumeAfterSuspend,e._autoResume())};e.ctx.suspend().then(n,n)}},3e4),e}},_autoResume:function(){var e=this;if(e.ctx&&void 0!==e.ctx.resume&&n.usingWebAudio)return"running"===e.state&&"interrupted"!==e.ctx.state&&e._suspendTimer?(clearTimeout(e._suspendTimer),e._suspendTimer=null):"suspended"===e.state||"running"===e.state&&"interrupted"===e.ctx.state?(e.ctx.resume().then(function(){e.state="running";for(var n=0;n<e._howls.length;n++)e._howls[n]._emit("resume")}),e._suspendTimer&&(clearTimeout(e._suspendTimer),e._suspendTimer=null)):"suspending"===e.state&&(e._resumeAfterSuspend=!0),e}};var n=new e,o=function(e){var n=this;if(!e.src||0===e.src.length)return void console.error("An array of source files must be passed with any new Howl.");n.init(e)};o.prototype={init:function(e){var o=this;return n.ctx||_(),o._autoplay=e.autoplay||!1,o._format="string"!=typeof e.format?e.format:[e.format],o._html5=e.html5||!1,o._muted=e.mute||!1,o._loop=e.loop||!1,o._pool=e.pool||5,o._preload="boolean"!=typeof e.preload&&"metadata"!==e.preload||e.preload,o._rate=e.rate||1,o._sprite=e.sprite||{},o._src="string"!=typeof e.src?e.src:[e.src],o._volume=void 0!==e.volume?e.volume:1,o._xhr={method:e.xhr&&e.xhr.method?e.xhr.method:"GET",headers:e.xhr&&e.xhr.headers?e.xhr.headers:null,withCredentials:!(!e.xhr||!e.xhr.withCredentials)&&e.xhr.withCredentials},o._duration=0,o._state="unloaded",o._sounds=[],o._endTimers={},o._queue=[],o._playLock=!1,o._onend=e.onend?[{fn:e.onend}]:[],o._onfade=e.onfade?[{fn:e.onfade}]:[],o._onload=e.onload?[{fn:e.onload}]:[],o._onloaderror=e.onloaderror?[{fn:e.onloaderror}]:[],o._onplayerror=e.onplayerror?[{fn:e.onplayerror}]:[],o._onpause=e.onpause?[{fn:e.onpause}]:[],o._onplay=e.onplay?[{fn:e.onplay}]:[],o._onstop=e.onstop?[{fn:e.onstop}]:[],o._onmute=e.onmute?[{fn:e.onmute}]:[],o._onvolume=e.onvolume?[{fn:e.onvolume}]:[],o._onrate=e.onrate?[{fn:e.onrate}]:[],o._onseek=e.onseek?[{fn:e.onseek}]:[],o._onunlock=e.onunlock?[{fn:e.onunlock}]:[],o._onresume=[],o._webAudio=n.usingWebAudio&&!o._html5,void 0!==n.ctx&&n.ctx&&n.autoUnlock&&n._unlockAudio(),n._howls.push(o),o._autoplay&&o._queue.push({event:"play",action:function(){o.play()}}),o._preload&&"none"!==o._preload&&o.load(),o},load:function(){var e=this,o=null;if(n.noAudio)return void e._emit("loaderror",null,"No audio support.");"string"==typeof e._src&&(e._src=[e._src]);for(var r=0;r<e._src.length;r++){var u,d;if(e._format&&e._format[r])u=e._format[r];else{if("string"!=typeof(d=e._src[r])){e._emit("loaderror",null,"Non-string found in selected audio sources - ignoring.");continue}u=/^data:audio\/([^;,]+);/i.exec(d),u||(u=/\.([^.]+)$/.exec(d.split("?",1)[0])),u&&(u=u[1].toLowerCase())}if(u||console.warn('No file extension was found. Consider using the "format" property or specify an extension.'),u&&n.codecs(u)){o=e._src[r];break}}return o?(e._src=o,e._state="loading","https:"===window.location.protocol&&"http:"===o.slice(0,5)&&(e._html5=!0,e._webAudio=!1),new t(e),e._webAudio&&a(e),e):void e._emit("loaderror",null,"No codec support for selected audio sources.")},play:function(e,o){var t=this,r=null;if("number"==typeof e)r=e,e=null;else{if("string"==typeof e&&"loaded"===t._state&&!t._sprite[e])return null;if(void 0===e&&(e="__default",!t._playLock)){for(var a=0,u=0;u<t._sounds.length;u++)t._sounds[u]._paused&&!t._sounds[u]._ended&&(a++,r=t._sounds[u]._id);1===a?e=null:r=null}}var d=r?t._soundById(r):t._inactiveSound();if(!d)return null;if(r&&!e&&(e=d._sprite||"__default"),"loaded"!==t._state){d._sprite=e,d._ended=!1;var i=d._id;return t._queue.push({event:"play",action:function(){t.play(i)}}),i}if(r&&!d._paused)return o||t._loadQueue("play"),d._id;t._webAudio&&n._autoResume();var _=Math.max(0,d._seek>0?d._seek:t._sprite[e][0]/1e3),s=Math.max(0,(t._sprite[e][0]+t._sprite[e][1])/1e3-_),l=1e3*s/Math.abs(d._rate),c=t._sprite[e][0]/1e3,f=(t._sprite[e][0]+t._sprite[e][1])/1e3;d._sprite=e,d._ended=!1;var p=function(){d._paused=!1,d._seek=_,d._start=c,d._stop=f,d._loop=!(!d._loop&&!t._sprite[e][2])};if(_>=f)return void t._ended(d);var m=d._node;if(t._webAudio){var v=function(){t._playLock=!1,p(),t._refreshBuffer(d);var e=d._muted||t._muted?0:d._volume;m.gain.setValueAtTime(e,n.ctx.currentTime),d._playStart=n.ctx.currentTime,void 0===m.bufferSource.start?d._loop?m.bufferSource.noteGrainOn(0,_,86400):m.bufferSource.noteGrainOn(0,_,s):d._loop?m.bufferSource.start(0,_,86400):m.bufferSource.start(0,_,s),l!==1/0&&(t._endTimers[d._id]=setTimeout(t._ended.bind(t,d),l)),o||setTimeout(function(){t._emit("play",d._id),t._loadQueue()},0)};"running"===n.state&&"interrupted"!==n.ctx.state?v():(t._playLock=!0,t.once("resume",v),t._clearTimer(d._id))}else{var h=function(){m.currentTime=_,m.muted=d._muted||t._muted||n._muted||m.muted,m.volume=d._volume*n.volume(),m.playbackRate=d._rate;try{var r=m.play();if(r&&"undefined"!=typeof Promise&&(r instanceof Promise||"function"==typeof r.then)?(t._playLock=!0,p(),r.then(function(){t._playLock=!1,m._unlocked=!0,o?t._loadQueue():t._emit("play",d._id)}).catch(function(){t._playLock=!1,t._emit("playerror",d._id,"Playback was unable to start. This is most commonly an issue on mobile devices and Chrome where playback was not within a user interaction."),d._ended=!0,d._paused=!0})):o||(t._playLock=!1,p(),t._emit("play",d._id)),m.playbackRate=d._rate,m.paused)return void t._emit("playerror",d._id,"Playback was unable to start. This is most commonly an issue on mobile devices and Chrome where playback was not within a user interaction.");"__default"!==e||d._loop?t._endTimers[d._id]=setTimeout(t._ended.bind(t,d),l):(t._endTimers[d._id]=function(){t._ended(d),m.removeEventListener("ended",t._endTimers[d._id],!1)},m.addEventListener("ended",t._endTimers[d._id],!1))}catch(e){t._emit("playerror",d._id,e)}};"data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA"===m.src&&(m.src=t._src,m.load());var y=window&&window.ejecta||!m.readyState&&n._navigator.isCocoonJS;if(m.readyState>=3||y)h();else{t._playLock=!0,t._state="loading";var g=function(){t._state="loaded",h(),m.removeEventListener(n._canPlayEvent,g,!1)};m.addEventListener(n._canPlayEvent,g,!1),t._clearTimer(d._id)}}return d._id},pause:function(e){var n=this;if("loaded"!==n._state||n._playLock)return n._queue.push({event:"pause",action:function(){n.pause(e)}}),n;for(var o=n._getSoundIds(e),t=0;t<o.length;t++){n._clearTimer(o[t]);var r=n._soundById(o[t]);if(r&&!r._paused&&(r._seek=n.seek(o[t]),r._rateSeek=0,r._paused=!0,n._stopFade(o[t]),r._node))if(n._webAudio){if(!r._node.bufferSource)continue;void 0===r._node.bufferSource.stop?r._node.bufferSource.noteOff(0):r._node.bufferSource.stop(0),n._cleanBuffer(r._node)}else isNaN(r._node.duration)&&r._node.duration!==1/0||r._node.pause();arguments[1]||n._emit("pause",r?r._id:null)}return n},stop:function(e,n){var o=this;if("loaded"!==o._state||o._playLock)return o._queue.push({event:"stop",action:function(){o.stop(e)}}),o;for(var t=o._getSoundIds(e),r=0;r<t.length;r++){o._clearTimer(t[r]);var a=o._soundById(t[r]);a&&(a._seek=a._start||0,a._rateSeek=0,a._paused=!0,a._ended=!0,o._stopFade(t[r]),a._node&&(o._webAudio?a._node.bufferSource&&(void 0===a._node.bufferSource.stop?a._node.bufferSource.noteOff(0):a._node.bufferSource.stop(0),o._cleanBuffer(a._node)):isNaN(a._node.duration)&&a._node.duration!==1/0||(a._node.currentTime=a._start||0,a._node.pause(),a._node.duration===1/0&&o._clearSound(a._node))),n||o._emit("stop",a._id))}return o},mute:function(e,o){var t=this;if("loaded"!==t._state||t._playLock)return t._queue.push({event:"mute",action:function(){t.mute(e,o)}}),t;if(void 0===o){if("boolean"!=typeof e)return t._muted;t._muted=e}for(var r=t._getSoundIds(o),a=0;a<r.length;a++){var u=t._soundById(r[a]);u&&(u._muted=e,u._interval&&t._stopFade(u._id),t._webAudio&&u._node?u._node.gain.setValueAtTime(e?0:u._volume,n.ctx.currentTime):u._node&&(u._node.muted=!!n._muted||e),t._emit("mute",u._id))}return t},volume:function(){var e,o,t=this,r=arguments;if(0===r.length)return t._volume;if(1===r.length||2===r.length&&void 0===r[1]){t._getSoundIds().indexOf(r[0])>=0?o=parseInt(r[0],10):e=parseFloat(r[0])}else r.length>=2&&(e=parseFloat(r[0]),o=parseInt(r[1],10));var a;if(!(void 0!==e&&e>=0&&e<=1))return a=o?t._soundById(o):t._sounds[0],a?a._volume:0;if("loaded"!==t._state||t._playLock)return t._queue.push({event:"volume",action:function(){t.volume.apply(t,r)}}),t;void 0===o&&(t._volume=e),o=t._getSoundIds(o);for(var u=0;u<o.length;u++)(a=t._soundById(o[u]))&&(a._volume=e,r[2]||t._stopFade(o[u]),t._webAudio&&a._node&&!a._muted?a._node.gain.setValueAtTime(e,n.ctx.currentTime):a._node&&!a._muted&&(a._node.volume=e*n.volume()),t._emit("volume",a._id));return t},fade:function(e,o,t,r){var a=this;if("loaded"!==a._state||a._playLock)return a._queue.push({event:"fade",action:function(){a.fade(e,o,t,r)}}),a;e=Math.min(Math.max(0,parseFloat(e)),1),o=Math.min(Math.max(0,parseFloat(o)),1),t=parseFloat(t),a.volume(e,r);for(var u=a._getSoundIds(r),d=0;d<u.length;d++){var i=a._soundById(u[d]);if(i){if(r||a._stopFade(u[d]),a._webAudio&&!i._muted){var _=n.ctx.currentTime,s=_+t/1e3;i._volume=e,i._node.gain.setValueAtTime(e,_),i._node.gain.linearRampToValueAtTime(o,s)}a._startFadeInterval(i,e,o,t,u[d],void 0===r)}}return a},_startFadeInterval:function(e,n,o,t,r,a){var u=this,d=n,i=o-n,_=Math.abs(i/.01),s=Math.max(4,_>0?t/_:t),l=Date.now();e._fadeTo=o,e._interval=setInterval(function(){var r=(Date.now()-l)/t;l=Date.now(),d+=i*r,d=Math.round(100*d)/100,d=i<0?Math.max(o,d):Math.min(o,d),u._webAudio?e._volume=d:u.volume(d,e._id,!0),a&&(u._volume=d),(o<n&&d<=o||o>n&&d>=o)&&(clearInterval(e._interval),e._interval=null,e._fadeTo=null,u.volume(o,e._id),u._emit("fade",e._id))},s)},_stopFade:function(e){var o=this,t=o._soundById(e);return t&&t._interval&&(o._webAudio&&t._node.gain.cancelScheduledValues(n.ctx.currentTime),clearInterval(t._interval),t._interval=null,o.volume(t._fadeTo,e),t._fadeTo=null,o._emit("fade",e)),o},loop:function(){var e,n,o,t=this,r=arguments;if(0===r.length)return t._loop;if(1===r.length){if("boolean"!=typeof r[0])return!!(o=t._soundById(parseInt(r[0],10)))&&o._loop;e=r[0],t._loop=e}else 2===r.length&&(e=r[0],n=parseInt(r[1],10));for(var a=t._getSoundIds(n),u=0;u<a.length;u++)(o=t._soundById(a[u]))&&(o._loop=e,t._webAudio&&o._node&&o._node.bufferSource&&(o._node.bufferSource.loop=e,e&&(o._node.bufferSource.loopStart=o._start||0,o._node.bufferSource.loopEnd=o._stop,t.playing(a[u])&&(t.pause(a[u],!0),t.play(a[u],!0)))));return t},rate:function(){var e,o,t=this,r=arguments;if(0===r.length)o=t._sounds[0]._id;else if(1===r.length){var a=t._getSoundIds(),u=a.indexOf(r[0]);u>=0?o=parseInt(r[0],10):e=parseFloat(r[0])}else 2===r.length&&(e=parseFloat(r[0]),o=parseInt(r[1],10));var d;if("number"!=typeof e)return d=t._soundById(o),d?d._rate:t._rate;if("loaded"!==t._state||t._playLock)return t._queue.push({event:"rate",action:function(){t.rate.apply(t,r)}}),t;void 0===o&&(t._rate=e),o=t._getSoundIds(o);for(var i=0;i<o.length;i++)if(d=t._soundById(o[i])){t.playing(o[i])&&(d._rateSeek=t.seek(o[i]),d._playStart=t._webAudio?n.ctx.currentTime:d._playStart),d._rate=e,t._webAudio&&d._node&&d._node.bufferSource?d._node.bufferSource.playbackRate.setValueAtTime(e,n.ctx.currentTime):d._node&&(d._node.playbackRate=e);var _=t.seek(o[i]),s=(t._sprite[d._sprite][0]+t._sprite[d._sprite][1])/1e3-_,l=1e3*s/Math.abs(d._rate);!t._endTimers[o[i]]&&d._paused||(t._clearTimer(o[i]),t._endTimers[o[i]]=setTimeout(t._ended.bind(t,d),l)),t._emit("rate",d._id)}return t},seek:function(){var e,o,t=this,r=arguments;if(0===r.length)t._sounds.length&&(o=t._sounds[0]._id);else if(1===r.length){var a=t._getSoundIds(),u=a.indexOf(r[0]);u>=0?o=parseInt(r[0],10):t._sounds.length&&(o=t._sounds[0]._id,e=parseFloat(r[0]))}else 2===r.length&&(e=parseFloat(r[0]),o=parseInt(r[1],10));if(void 0===o)return 0;if("number"==typeof e&&("loaded"!==t._state||t._playLock))return t._queue.push({event:"seek",action:function(){t.seek.apply(t,r)}}),t;var d=t._soundById(o);if(d){if(!("number"==typeof e&&e>=0)){if(t._webAudio){var i=t.playing(o)?n.ctx.currentTime-d._playStart:0,_=d._rateSeek?d._rateSeek-d._seek:0;return d._seek+(_+i*Math.abs(d._rate))}return d._node.currentTime}var s=t.playing(o);s&&t.pause(o,!0),d._seek=e,d._ended=!1,t._clearTimer(o),t._webAudio||!d._node||isNaN(d._node.duration)||(d._node.currentTime=e);var l=function(){s&&t.play(o,!0),t._emit("seek",o)};if(s&&!t._webAudio){var c=function(){t._playLock?setTimeout(c,0):l()};setTimeout(c,0)}else l()}return t},playing:function(e){var n=this;if("number"==typeof e){var o=n._soundById(e);return!!o&&!o._paused}for(var t=0;t<n._sounds.length;t++)if(!n._sounds[t]._paused)return!0;return!1},duration:function(e){var n=this,o=n._duration,t=n._soundById(e);return t&&(o=n._sprite[t._sprite][1]/1e3),o},state:function(){return this._state},unload:function(){for(var e=this,o=e._sounds,t=0;t<o.length;t++)o[t]._paused||e.stop(o[t]._id),e._webAudio||(e._clearSound(o[t]._node),o[t]._node.removeEventListener("error",o[t]._errorFn,!1),o[t]._node.removeEventListener(n._canPlayEvent,o[t]._loadFn,!1),o[t]._node.removeEventListener("ended",o[t]._endFn,!1),n._releaseHtml5Audio(o[t]._node)),delete o[t]._node,e._clearTimer(o[t]._id);var a=n._howls.indexOf(e);a>=0&&n._howls.splice(a,1);var u=!0;for(t=0;t<n._howls.length;t++)if(n._howls[t]._src===e._src||e._src.indexOf(n._howls[t]._src)>=0){u=!1;break}return r&&u&&delete r[e._src],n.noAudio=!1,e._state="unloaded",e._sounds=[],e=null,null},on:function(e,n,o,t){var r=this,a=r["_on"+e];return"function"==typeof n&&a.push(t?{id:o,fn:n,once:t}:{id:o,fn:n}),r},off:function(e,n,o){var t=this,r=t["_on"+e],a=0;if("number"==typeof n&&(o=n,n=null),n||o)for(a=0;a<r.length;a++){var u=o===r[a].id;if(n===r[a].fn&&u||!n&&u){r.splice(a,1);break}}else if(e)t["_on"+e]=[];else{var d=Object.keys(t);for(a=0;a<d.length;a++)0===d[a].indexOf("_on")&&Array.isArray(t[d[a]])&&(t[d[a]]=[])}return t},once:function(e,n,o){var t=this;return t.on(e,n,o,1),t},_emit:function(e,n,o){for(var t=this,r=t["_on"+e],a=r.length-1;a>=0;a--)r[a].id&&r[a].id!==n&&"load"!==e||(setTimeout(function(e){e.call(this,n,o)}.bind(t,r[a].fn),0),r[a].once&&t.off(e,r[a].fn,r[a].id));return t._loadQueue(e),t},_loadQueue:function(e){var n=this;if(n._queue.length>0){var o=n._queue[0];o.event===e&&(n._queue.shift(),n._loadQueue()),e||o.action()}return n},_ended:function(e){var o=this,t=e._sprite;if(!o._webAudio&&e._node&&!e._node.paused&&!e._node.ended&&e._node.currentTime<e._stop)return setTimeout(o._ended.bind(o,e),100),o;var r=!(!e._loop&&!o._sprite[t][2]);if(o._emit("end",e._id),!o._webAudio&&r&&o.stop(e._id,!0).play(e._id),o._webAudio&&r){o._emit("play",e._id),e._seek=e._start||0,e._rateSeek=0,e._playStart=n.ctx.currentTime;var a=1e3*(e._stop-e._start)/Math.abs(e._rate);o._endTimers[e._id]=setTimeout(o._ended.bind(o,e),a)}return o._webAudio&&!r&&(e._paused=!0,e._ended=!0,e._seek=e._start||0,e._rateSeek=0,o._clearTimer(e._id),o._cleanBuffer(e._node),n._autoSuspend()),o._webAudio||r||o.stop(e._id,!0),o},_clearTimer:function(e){var n=this;if(n._endTimers[e]){if("function"!=typeof n._endTimers[e])clearTimeout(n._endTimers[e]);else{var o=n._soundById(e);o&&o._node&&o._node.removeEventListener("ended",n._endTimers[e],!1)}delete n._endTimers[e]}return n},_soundById:function(e){for(var n=this,o=0;o<n._sounds.length;o++)if(e===n._sounds[o]._id)return n._sounds[o];return null},_inactiveSound:function(){var e=this;e._drain();for(var n=0;n<e._sounds.length;n++)if(e._sounds[n]._ended)return e._sounds[n].reset();return new t(e)},_drain:function(){var e=this,n=e._pool,o=0,t=0;if(!(e._sounds.length<n)){for(t=0;t<e._sounds.length;t++)e._sounds[t]._ended&&o++;for(t=e._sounds.length-1;t>=0;t--){if(o<=n)return;e._sounds[t]._ended&&(e._webAudio&&e._sounds[t]._node&&e._sounds[t]._node.disconnect(0),e._sounds.splice(t,1),o--)}}},_getSoundIds:function(e){var n=this;if(void 0===e){for(var o=[],t=0;t<n._sounds.length;t++)o.push(n._sounds[t]._id);return o}return[e]},_refreshBuffer:function(e){var o=this;return e._node.bufferSource=n.ctx.createBufferSource(),e._node.bufferSource.buffer=r[o._src],e._panner?e._node.bufferSource.connect(e._panner):e._node.bufferSource.connect(e._node),e._node.bufferSource.loop=e._loop,e._loop&&(e._node.bufferSource.loopStart=e._start||0,e._node.bufferSource.loopEnd=e._stop||0),e._node.bufferSource.playbackRate.setValueAtTime(e._rate,n.ctx.currentTime),o},_cleanBuffer:function(e){var o=this,t=n._navigator&&n._navigator.vendor.indexOf("Apple")>=0;if(!e.bufferSource)return o;if(n._scratchBuffer&&e.bufferSource&&(e.bufferSource.onended=null,e.bufferSource.disconnect(0),t))try{e.bufferSource.buffer=n._scratchBuffer}catch(e){}return e.bufferSource=null,o},_clearSound:function(e){/MSIE |Trident\//.test(n._navigator&&n._navigator.userAgent)||(e.src="data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA")}};var t=function(e){this._parent=e,this.init()};t.prototype={init:function(){var e=this,o=e._parent;return e._muted=o._muted,e._loop=o._loop,e._volume=o._volume,e._rate=o._rate,e._seek=0,e._paused=!0,e._ended=!0,e._sprite="__default",e._id=++n._counter,o._sounds.push(e),e.create(),e},create:function(){var e=this,o=e._parent,t=n._muted||e._muted||e._parent._muted?0:e._volume;return o._webAudio?(e._node=void 0===n.ctx.createGain?n.ctx.createGainNode():n.ctx.createGain(),e._node.gain.setValueAtTime(t,n.ctx.currentTime),e._node.paused=!0,e._node.connect(n.masterGain)):n.noAudio||(e._node=n._obtainHtml5Audio(),e._errorFn=e._errorListener.bind(e),e._node.addEventListener("error",e._errorFn,!1),e._loadFn=e._loadListener.bind(e),e._node.addEventListener(n._canPlayEvent,e._loadFn,!1),e._endFn=e._endListener.bind(e),e._node.addEventListener("ended",e._endFn,!1),e._node.src=o._src,e._node.preload=!0===o._preload?"auto":o._preload,e._node.volume=t*n.volume(),e._node.load()),e},reset:function(){var e=this,o=e._parent;return e._muted=o._muted,e._loop=o._loop,e._volume=o._volume,e._rate=o._rate,e._seek=0,e._rateSeek=0,e._paused=!0,e._ended=!0,e._sprite="__default",e._id=++n._counter,e},_errorListener:function(){var e=this;e._parent._emit("loaderror",e._id,e._node.error?e._node.error.code:0),e._node.removeEventListener("error",e._errorFn,!1)},_loadListener:function(){var e=this,o=e._parent;o._duration=Math.ceil(10*e._node.duration)/10,0===Object.keys(o._sprite).length&&(o._sprite={__default:[0,1e3*o._duration]}),"loaded"!==o._state&&(o._state="loaded",o._emit("load"),o._loadQueue()),e._node.removeEventListener(n._canPlayEvent,e._loadFn,!1)},_endListener:function(){var e=this,n=e._parent;n._duration===1/0&&(n._duration=Math.ceil(10*e._node.duration)/10,n._sprite.__default[1]===1/0&&(n._sprite.__default[1]=1e3*n._duration),n._ended(e)),e._node.removeEventListener("ended",e._endFn,!1)}};var r={},a=function(e){var n=e._src;if(r[n])return e._duration=r[n].duration,void i(e);if(/^data:[^;]+;base64,/.test(n)){for(var o=atob(n.split(",")[1]),t=new Uint8Array(o.length),a=0;a<o.length;++a)t[a]=o.charCodeAt(a);d(t.buffer,e)}else{var _=new XMLHttpRequest;_.open(e._xhr.method,n,!0),_.withCredentials=e._xhr.withCredentials,_.responseType="arraybuffer",e._xhr.headers&&Object.keys(e._xhr.headers).forEach(function(n){_.setRequestHeader(n,e._xhr.headers[n])}),_.onload=function(){var n=(_.status+"")[0];if("0"!==n&&"2"!==n&&"3"!==n)return void e._emit("loaderror",null,"Failed loading audio file with status: "+_.status+".");d(_.response,e)},_.onerror=function(){e._webAudio&&(e._html5=!0,e._webAudio=!1,e._sounds=[],delete r[n],e.load())},u(_)}},u=function(e){try{e.send()}catch(n){e.onerror()}},d=function(e,o){var t=function(){o._emit("loaderror",null,"Decoding audio data failed.")},a=function(e){e&&o._sounds.length>0?(r[o._src]=e,i(o,e)):t()};"undefined"!=typeof Promise&&1===n.ctx.decodeAudioData.length?n.ctx.decodeAudioData(e).then(a).catch(t):n.ctx.decodeAudioData(e,a,t)},i=function(e,n){n&&!e._duration&&(e._duration=n.duration),0===Object.keys(e._sprite).length&&(e._sprite={__default:[0,1e3*e._duration]}),"loaded"!==e._state&&(e._state="loaded",e._emit("load"),e._loadQueue())},_=function(){if(n.usingWebAudio){try{"undefined"!=typeof AudioContext?n.ctx=new AudioContext:"undefined"!=typeof webkitAudioContext?n.ctx=new webkitAudioContext:n.usingWebAudio=!1}catch(e){n.usingWebAudio=!1}n.ctx||(n.usingWebAudio=!1);var e=/iP(hone|od|ad)/.test(n._navigator&&n._navigator.platform),o=n._navigator&&n._navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/),t=o?parseInt(o[1],10):null;if(e&&t&&t<9){var r=/safari/.test(n._navigator&&n._navigator.userAgent.toLowerCase());n._navigator&&!r&&(n.usingWebAudio=!1)}n.usingWebAudio&&(n.masterGain=void 0===n.ctx.createGain?n.ctx.createGainNode():n.ctx.createGain(),n.masterGain.gain.setValueAtTime(n._muted?0:n._volume,n.ctx.currentTime),n.masterGain.connect(n.ctx.destination)),n._setup()}};"function"==typeof define&&define.amd&&define([],function(){return{Howler:n,Howl:o}}),"undefined"!=typeof exports&&(exports.Howler=n,exports.Howl=o),"undefined"!=typeof global?(global.HowlerGlobal=e,global.Howler=n,global.Howl=o,global.Sound=t):"undefined"!=typeof window&&(window.HowlerGlobal=e,window.Howler=n,window.Howl=o,window.Sound=t)}();
|
| 3 |
+
/*! Spatial Plugin */
|
| 4 |
+
!function(){"use strict";HowlerGlobal.prototype._pos=[0,0,0],HowlerGlobal.prototype._orientation=[0,0,-1,0,1,0],HowlerGlobal.prototype.stereo=function(e){var n=this;if(!n.ctx||!n.ctx.listener)return n;for(var t=n._howls.length-1;t>=0;t--)n._howls[t].stereo(e);return n},HowlerGlobal.prototype.pos=function(e,n,t){var r=this;return r.ctx&&r.ctx.listener?(n="number"!=typeof n?r._pos[1]:n,t="number"!=typeof t?r._pos[2]:t,"number"!=typeof e?r._pos:(r._pos=[e,n,t],void 0!==r.ctx.listener.positionX?(r.ctx.listener.positionX.setTargetAtTime(r._pos[0],Howler.ctx.currentTime,.1),r.ctx.listener.positionY.setTargetAtTime(r._pos[1],Howler.ctx.currentTime,.1),r.ctx.listener.positionZ.setTargetAtTime(r._pos[2],Howler.ctx.currentTime,.1)):r.ctx.listener.setPosition(r._pos[0],r._pos[1],r._pos[2]),r)):r},HowlerGlobal.prototype.orientation=function(e,n,t,r,o,i){var a=this;if(!a.ctx||!a.ctx.listener)return a;var p=a._orientation;return n="number"!=typeof n?p[1]:n,t="number"!=typeof t?p[2]:t,r="number"!=typeof r?p[3]:r,o="number"!=typeof o?p[4]:o,i="number"!=typeof i?p[5]:i,"number"!=typeof e?p:(a._orientation=[e,n,t,r,o,i],void 0!==a.ctx.listener.forwardX?(a.ctx.listener.forwardX.setTargetAtTime(e,Howler.ctx.currentTime,.1),a.ctx.listener.forwardY.setTargetAtTime(n,Howler.ctx.currentTime,.1),a.ctx.listener.forwardZ.setTargetAtTime(t,Howler.ctx.currentTime,.1),a.ctx.listener.upX.setTargetAtTime(r,Howler.ctx.currentTime,.1),a.ctx.listener.upY.setTargetAtTime(o,Howler.ctx.currentTime,.1),a.ctx.listener.upZ.setTargetAtTime(i,Howler.ctx.currentTime,.1)):a.ctx.listener.setOrientation(e,n,t,r,o,i),a)},Howl.prototype.init=function(e){return function(n){var t=this;return t._orientation=n.orientation||[1,0,0],t._stereo=n.stereo||null,t._pos=n.pos||null,t._pannerAttr={coneInnerAngle:void 0!==n.coneInnerAngle?n.coneInnerAngle:360,coneOuterAngle:void 0!==n.coneOuterAngle?n.coneOuterAngle:360,coneOuterGain:void 0!==n.coneOuterGain?n.coneOuterGain:0,distanceModel:void 0!==n.distanceModel?n.distanceModel:"inverse",maxDistance:void 0!==n.maxDistance?n.maxDistance:1e4,panningModel:void 0!==n.panningModel?n.panningModel:"HRTF",refDistance:void 0!==n.refDistance?n.refDistance:1,rolloffFactor:void 0!==n.rolloffFactor?n.rolloffFactor:1},t._onstereo=n.onstereo?[{fn:n.onstereo}]:[],t._onpos=n.onpos?[{fn:n.onpos}]:[],t._onorientation=n.onorientation?[{fn:n.onorientation}]:[],e.call(this,n)}}(Howl.prototype.init),Howl.prototype.stereo=function(n,t){var r=this;if(!r._webAudio)return r;if("loaded"!==r._state)return r._queue.push({event:"stereo",action:function(){r.stereo(n,t)}}),r;var o=void 0===Howler.ctx.createStereoPanner?"spatial":"stereo";if(void 0===t){if("number"!=typeof n)return r._stereo;r._stereo=n,r._pos=[n,0,0]}for(var i=r._getSoundIds(t),a=0;a<i.length;a++){var p=r._soundById(i[a]);if(p){if("number"!=typeof n)return p._stereo;p._stereo=n,p._pos=[n,0,0],p._node&&(p._pannerAttr.panningModel="equalpower",p._panner&&p._panner.pan||e(p,o),"spatial"===o?void 0!==p._panner.positionX?(p._panner.positionX.setValueAtTime(n,Howler.ctx.currentTime),p._panner.positionY.setValueAtTime(0,Howler.ctx.currentTime),p._panner.positionZ.setValueAtTime(0,Howler.ctx.currentTime)):p._panner.setPosition(n,0,0):p._panner.pan.setValueAtTime(n,Howler.ctx.currentTime)),r._emit("stereo",p._id)}}return r},Howl.prototype.pos=function(n,t,r,o){var i=this;if(!i._webAudio)return i;if("loaded"!==i._state)return i._queue.push({event:"pos",action:function(){i.pos(n,t,r,o)}}),i;if(t="number"!=typeof t?0:t,r="number"!=typeof r?-.5:r,void 0===o){if("number"!=typeof n)return i._pos;i._pos=[n,t,r]}for(var a=i._getSoundIds(o),p=0;p<a.length;p++){var s=i._soundById(a[p]);if(s){if("number"!=typeof n)return s._pos;s._pos=[n,t,r],s._node&&(s._panner&&!s._panner.pan||e(s,"spatial"),void 0!==s._panner.positionX?(s._panner.positionX.setValueAtTime(n,Howler.ctx.currentTime),s._panner.positionY.setValueAtTime(t,Howler.ctx.currentTime),s._panner.positionZ.setValueAtTime(r,Howler.ctx.currentTime)):s._panner.setPosition(n,t,r)),i._emit("pos",s._id)}}return i},Howl.prototype.orientation=function(n,t,r,o){var i=this;if(!i._webAudio)return i;if("loaded"!==i._state)return i._queue.push({event:"orientation",action:function(){i.orientation(n,t,r,o)}}),i;if(t="number"!=typeof t?i._orientation[1]:t,r="number"!=typeof r?i._orientation[2]:r,void 0===o){if("number"!=typeof n)return i._orientation;i._orientation=[n,t,r]}for(var a=i._getSoundIds(o),p=0;p<a.length;p++){var s=i._soundById(a[p]);if(s){if("number"!=typeof n)return s._orientation;s._orientation=[n,t,r],s._node&&(s._panner||(s._pos||(s._pos=i._pos||[0,0,-.5]),e(s,"spatial")),void 0!==s._panner.orientationX?(s._panner.orientationX.setValueAtTime(n,Howler.ctx.currentTime),s._panner.orientationY.setValueAtTime(t,Howler.ctx.currentTime),s._panner.orientationZ.setValueAtTime(r,Howler.ctx.currentTime)):s._panner.setOrientation(n,t,r)),i._emit("orientation",s._id)}}return i},Howl.prototype.pannerAttr=function(){var n,t,r,o=this,i=arguments;if(!o._webAudio)return o;if(0===i.length)return o._pannerAttr;if(1===i.length){if("object"!=typeof i[0])return r=o._soundById(parseInt(i[0],10)),r?r._pannerAttr:o._pannerAttr;n=i[0],void 0===t&&(n.pannerAttr||(n.pannerAttr={coneInnerAngle:n.coneInnerAngle,coneOuterAngle:n.coneOuterAngle,coneOuterGain:n.coneOuterGain,distanceModel:n.distanceModel,maxDistance:n.maxDistance,refDistance:n.refDistance,rolloffFactor:n.rolloffFactor,panningModel:n.panningModel}),o._pannerAttr={coneInnerAngle:void 0!==n.pannerAttr.coneInnerAngle?n.pannerAttr.coneInnerAngle:o._coneInnerAngle,coneOuterAngle:void 0!==n.pannerAttr.coneOuterAngle?n.pannerAttr.coneOuterAngle:o._coneOuterAngle,coneOuterGain:void 0!==n.pannerAttr.coneOuterGain?n.pannerAttr.coneOuterGain:o._coneOuterGain,distanceModel:void 0!==n.pannerAttr.distanceModel?n.pannerAttr.distanceModel:o._distanceModel,maxDistance:void 0!==n.pannerAttr.maxDistance?n.pannerAttr.maxDistance:o._maxDistance,refDistance:void 0!==n.pannerAttr.refDistance?n.pannerAttr.refDistance:o._refDistance,rolloffFactor:void 0!==n.pannerAttr.rolloffFactor?n.pannerAttr.rolloffFactor:o._rolloffFactor,panningModel:void 0!==n.pannerAttr.panningModel?n.pannerAttr.panningModel:o._panningModel})}else 2===i.length&&(n=i[0],t=parseInt(i[1],10));for(var a=o._getSoundIds(t),p=0;p<a.length;p++)if(r=o._soundById(a[p])){var s=r._pannerAttr;s={coneInnerAngle:void 0!==n.coneInnerAngle?n.coneInnerAngle:s.coneInnerAngle,coneOuterAngle:void 0!==n.coneOuterAngle?n.coneOuterAngle:s.coneOuterAngle,coneOuterGain:void 0!==n.coneOuterGain?n.coneOuterGain:s.coneOuterGain,distanceModel:void 0!==n.distanceModel?n.distanceModel:s.distanceModel,maxDistance:void 0!==n.maxDistance?n.maxDistance:s.maxDistance,refDistance:void 0!==n.refDistance?n.refDistance:s.refDistance,rolloffFactor:void 0!==n.rolloffFactor?n.rolloffFactor:s.rolloffFactor,panningModel:void 0!==n.panningModel?n.panningModel:s.panningModel};var c=r._panner;c||(r._pos||(r._pos=o._pos||[0,0,-.5]),e(r,"spatial"),c=r._panner),c.coneInnerAngle=s.coneInnerAngle,c.coneOuterAngle=s.coneOuterAngle,c.coneOuterGain=s.coneOuterGain,c.distanceModel=s.distanceModel,c.maxDistance=s.maxDistance,c.refDistance=s.refDistance,c.rolloffFactor=s.rolloffFactor,c.panningModel=s.panningModel}return o},Sound.prototype.init=function(e){return function(){var n=this,t=n._parent;n._orientation=t._orientation,n._stereo=t._stereo,n._pos=t._pos,n._pannerAttr=t._pannerAttr,e.call(this),n._stereo?t.stereo(n._stereo):n._pos&&t.pos(n._pos[0],n._pos[1],n._pos[2],n._id)}}(Sound.prototype.init),Sound.prototype.reset=function(e){return function(){var n=this,t=n._parent;return n._orientation=t._orientation,n._stereo=t._stereo,n._pos=t._pos,n._pannerAttr=t._pannerAttr,n._stereo?t.stereo(n._stereo):n._pos?t.pos(n._pos[0],n._pos[1],n._pos[2],n._id):n._panner&&(n._panner.disconnect(0),n._panner=void 0,t._refreshBuffer(n)),e.call(this)}}(Sound.prototype.reset);var e=function(e,n){n=n||"spatial","spatial"===n?(e._panner=Howler.ctx.createPanner(),e._panner.coneInnerAngle=e._pannerAttr.coneInnerAngle,e._panner.coneOuterAngle=e._pannerAttr.coneOuterAngle,e._panner.coneOuterGain=e._pannerAttr.coneOuterGain,e._panner.distanceModel=e._pannerAttr.distanceModel,e._panner.maxDistance=e._pannerAttr.maxDistance,e._panner.refDistance=e._pannerAttr.refDistance,e._panner.rolloffFactor=e._pannerAttr.rolloffFactor,e._panner.panningModel=e._pannerAttr.panningModel,void 0!==e._panner.positionX?(e._panner.positionX.setValueAtTime(e._pos[0],Howler.ctx.currentTime),e._panner.positionY.setValueAtTime(e._pos[1],Howler.ctx.currentTime),e._panner.positionZ.setValueAtTime(e._pos[2],Howler.ctx.currentTime)):e._panner.setPosition(e._pos[0],e._pos[1],e._pos[2]),void 0!==e._panner.orientationX?(e._panner.orientationX.setValueAtTime(e._orientation[0],Howler.ctx.currentTime),e._panner.orientationY.setValueAtTime(e._orientation[1],Howler.ctx.currentTime),e._panner.orientationZ.setValueAtTime(e._orientation[2],Howler.ctx.currentTime)):e._panner.setOrientation(e._orientation[0],e._orientation[1],e._orientation[2])):(e._panner=Howler.ctx.createStereoPanner(),e._panner.pan.setValueAtTime(e._stereo,Howler.ctx.currentTime)),e._panner.connect(e._node),e._paused||e._parent.pause(e._id,!0).play(e._id,!0)}}();
|
static/js/vue.global.prod.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|