Tadeas Kosek commited on
Commit
f1a7e59
·
1 Parent(s): 92fd1a7

replace frontend with swagger

Browse files
Dockerfile CHANGED
@@ -4,8 +4,6 @@ FROM python:3.9-slim
4
  RUN apt-get update && \
5
  apt-get install -y \
6
  ffmpeg \
7
- libsm6 \
8
- libxext6 \
9
  curl \
10
  && apt-get clean \
11
  && rm -rf /var/lib/apt/lists/*
@@ -25,13 +23,11 @@ ENV PATH="/home/user/.local/bin:$PATH"
25
  RUN pip install --no-cache-dir --upgrade pip && \
26
  pip install --no-cache-dir -r requirements.txt
27
 
28
- # Copy application code maintaining structure
29
  COPY --chown=user:user . .
30
 
31
  # Create necessary directories
32
- RUN mkdir -p /tmp/audio_extractor && \
33
- mkdir -p interfaces/web/static && \
34
- mkdir -p interfaces/web/templates
35
 
36
  # Set environment variables
37
  ENV PYTHONPATH=/app
 
4
  RUN apt-get update && \
5
  apt-get install -y \
6
  ffmpeg \
 
 
7
  curl \
8
  && apt-get clean \
9
  && rm -rf /var/lib/apt/lists/*
 
23
  RUN pip install --no-cache-dir --upgrade pip && \
24
  pip install --no-cache-dir -r requirements.txt
25
 
26
+ # Copy application code
27
  COPY --chown=user:user . .
28
 
29
  # Create necessary directories
30
+ RUN mkdir -p /tmp/audio_extractor
 
 
31
 
32
  # Set environment variables
33
  ENV PYTHONPATH=/app
app.py CHANGED
@@ -1,7 +1,6 @@
1
- """FastAPI application initialization with DDD architecture."""
2
- from fastapi import FastAPI, Request
3
- from fastapi.staticfiles import StaticFiles
4
- from fastapi.templating import Jinja2Templates
5
  from contextlib import asynccontextmanager
6
  import logging
7
 
@@ -43,23 +42,41 @@ async def lifespan(app: FastAPI):
43
  # Start background services
44
  await service_container.startup()
45
 
46
- logger.info("Application started successfully")
47
 
48
  yield
49
 
50
  # Shutdown
51
- logger.info("Shutting down application")
52
 
53
  # Stop background services
54
  await service_container.shutdown()
55
 
56
- logger.info("Application shut down successfully")
57
 
58
- # Create FastAPI app
59
  app = FastAPI(
60
  title=settings.app_name,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  version=settings.app_version,
62
- description="Extract audio from video files using FFmpeg",
 
 
63
  lifespan=lifespan
64
  )
65
 
@@ -70,17 +87,11 @@ register_exception_handlers(app)
70
  # Register API routes
71
  register_routes(app)
72
 
73
- # Mount static files
74
- app.mount("/static", StaticFiles(directory="interfaces/web/static"), name="static")
75
-
76
- # Setup templates
77
- templates = Jinja2Templates(directory="interfaces/web/templates")
78
-
79
- # Root route for web interface
80
  @app.get("/", include_in_schema=False)
81
- async def home(request: Request):
82
- """Serve the web interface."""
83
- return templates.TemplateResponse("index.html", {"request": request})
84
 
85
  # Dependency injection functions for routes
86
  def get_service_container() -> ServiceContainer:
 
1
+ """FastAPI API-only application with DDD architecture."""
2
+ from fastapi import FastAPI
3
+ from fastapi.responses import RedirectResponse
 
4
  from contextlib import asynccontextmanager
5
  import logging
6
 
 
42
  # Start background services
43
  await service_container.startup()
44
 
45
+ logger.info("API service started successfully")
46
 
47
  yield
48
 
49
  # Shutdown
50
+ logger.info("Shutting down API service")
51
 
52
  # Stop background services
53
  await service_container.shutdown()
54
 
55
+ logger.info("API service shut down successfully")
56
 
57
+ # Create FastAPI app with API documentation
58
  app = FastAPI(
59
  title=settings.app_name,
60
+ description="""
61
+ ## Audio Extraction API
62
+
63
+ Extract audio from video files in various formats with customizable quality settings.
64
+
65
+ ### Features
66
+ - 🎥 Support for multiple video formats (MP4, AVI, MOV, MKV, WebM, etc.)
67
+ - 🎵 Multiple audio output formats (MP3, AAC, WAV, FLAC, M4A, OGG)
68
+ - 📊 Three quality levels (High, Medium, Low)
69
+ - 🚀 Automatic handling based on file size
70
+ - 📦 Async processing for large files
71
+
72
+ ### Processing Flow
73
+ 1. **Small files (<10MB)**: Direct processing with immediate response
74
+ 2. **Large files (≥10MB)**: Background processing with job tracking
75
+ """,
76
  version=settings.app_version,
77
+ docs_url="/docs",
78
+ redoc_url="/redoc",
79
+ openapi_url="/openapi.json",
80
  lifespan=lifespan
81
  )
82
 
 
87
  # Register API routes
88
  register_routes(app)
89
 
90
+ # Root redirect to documentation
 
 
 
 
 
 
91
  @app.get("/", include_in_schema=False)
92
+ async def root():
93
+ """Redirect root to API documentation."""
94
+ return RedirectResponse(url="/docs")
95
 
96
  # Dependency injection functions for routes
97
  def get_service_container() -> ServiceContainer:
interfaces/api/routes/extraction_routes.py CHANGED
@@ -1,5 +1,5 @@
1
  """Audio extraction API routes."""
2
- from fastapi import APIRouter, BackgroundTasks, Response, Request, UploadFile
3
  from fastapi.responses import FileResponse, JSONResponse
4
  from typing import Dict, Any
5
  from dataclasses import asdict
@@ -10,7 +10,27 @@ from application.dto.extraction_request import ExtractionRequestDTO
10
 
11
  router = APIRouter()
12
 
13
- @router.post("/extract", response_model=None)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  async def extract_audio(
15
  background_tasks: BackgroundTasks,
16
  video: ValidatedVideo,
@@ -21,8 +41,9 @@ async def extract_audio(
21
  """
22
  Extract audio from uploaded video file.
23
 
24
- For small files (<10MB), returns the audio file directly.
25
- For large files, returns a job ID for async processing.
 
26
  """
27
  # Get file size
28
  file_size = _get_file_size(video)
 
1
  """Audio extraction API routes."""
2
+ from fastapi import APIRouter, BackgroundTasks, Response, Request, File, UploadFile, Form
3
  from fastapi.responses import FileResponse, JSONResponse
4
  from typing import Dict, Any
5
  from dataclasses import asdict
 
10
 
11
  router = APIRouter()
12
 
13
+ @router.post("/extract",
14
+ response_model=None,
15
+ summary="Extract Audio from Video",
16
+ description="""
17
+ Extract audio from uploaded video file.
18
+
19
+ - **Small files (<10MB)**: Returns the audio file directly
20
+ - **Large files (≥10MB)**: Returns a job ID for async processing
21
+ """,
22
+ responses={
23
+ 200: {
24
+ "description": "Direct audio file response (small files)",
25
+ "content": {"audio/mpeg": {}, "audio/aac": {}, "audio/wav": {}}
26
+ },
27
+ 202: {
28
+ "description": "Job created for async processing (large files)",
29
+ "model": JobCreatedResponse
30
+ },
31
+ 400: {"description": "Invalid input"},
32
+ 500: {"description": "Processing error"}
33
+ })
34
  async def extract_audio(
35
  background_tasks: BackgroundTasks,
36
  video: ValidatedVideo,
 
41
  """
42
  Extract audio from uploaded video file.
43
 
44
+ The API automatically determines the processing method based on file size:
45
+ - Files under 10MB are processed immediately and the audio file is returned
46
+ - Files 10MB or larger are processed asynchronously, returning a job ID for tracking
47
  """
48
  # Get file size
49
  file_size = _get_file_size(video)
interfaces/api/routes/info_routes.py CHANGED
@@ -6,7 +6,10 @@ from ..responses import ApiInfoResponse
6
 
7
  router = APIRouter()
8
 
9
- @router.get("/info", response_model=ApiInfoResponse)
 
 
 
10
  async def get_api_info():
11
  """Get API information and supported formats."""
12
  return ApiInfoResponse(
@@ -19,11 +22,15 @@ async def get_api_info():
19
  "/api/v1/extract": "POST - Extract audio from video",
20
  "/api/v1/jobs/{job_id}": "GET - Check job status",
21
  "/api/v1/jobs/{job_id}/download": "GET - Download processed audio",
22
- "/api/v1/info": "GET - API information"
 
23
  }
24
  )
25
 
26
- @router.get("/health")
 
 
 
27
  async def health_check():
28
  """Simple health check endpoint."""
29
- return {"status": "healthy", "service": "audio-extractor"}
 
6
 
7
  router = APIRouter()
8
 
9
+ @router.get("/info",
10
+ response_model=ApiInfoResponse,
11
+ summary="Get API Information",
12
+ description="Returns information about supported formats, quality levels, and available endpoints")
13
  async def get_api_info():
14
  """Get API information and supported formats."""
15
  return ApiInfoResponse(
 
22
  "/api/v1/extract": "POST - Extract audio from video",
23
  "/api/v1/jobs/{job_id}": "GET - Check job status",
24
  "/api/v1/jobs/{job_id}/download": "GET - Download processed audio",
25
+ "/api/v1/info": "GET - API information",
26
+ "/api/v1/health": "GET - Health check"
27
  }
28
  )
29
 
30
+ @router.get("/health",
31
+ summary="Health Check",
32
+ description="Simple health check endpoint for monitoring",
33
+ response_model=Dict[str, str])
34
  async def health_check():
35
  """Simple health check endpoint."""
36
+ return {"status": "healthy", "service": "audio-extractor-api"}
interfaces/api/routes/job_routes.py CHANGED
@@ -1,5 +1,5 @@
1
  """Job management API routes."""
2
- from fastapi import APIRouter, HTTPException
3
  from fastapi.responses import FileResponse
4
  from typing import Any
5
 
@@ -9,9 +9,16 @@ from domain.exceptions.domain_exceptions import JobNotFoundError, JobNotComplete
9
 
10
  router = APIRouter()
11
 
12
- @router.get("/jobs/{job_id}", response_model=JobStatusResponse)
 
 
 
 
 
 
 
13
  async def get_job_status(
14
- job_id: str,
15
  use_cases: UseCases
16
  ):
17
  """Get the status of a background processing job."""
@@ -36,9 +43,19 @@ async def get_job_status(
36
  except Exception as e:
37
  raise HTTPException(500, f"Error checking job status: {str(e)}")
38
 
39
- @router.get("/jobs/{job_id}/download")
 
 
 
 
 
 
 
 
 
 
40
  async def download_job_result(
41
- job_id: str,
42
  use_cases: UseCases
43
  ):
44
  """Download the result of a completed job."""
 
1
  """Job management API routes."""
2
+ from fastapi import APIRouter, HTTPException, Request, Path
3
  from fastapi.responses import FileResponse
4
  from typing import Any
5
 
 
9
 
10
  router = APIRouter()
11
 
12
+ @router.get("/jobs/{job_id}",
13
+ response_model=JobStatusResponse,
14
+ summary="Get Job Status",
15
+ description="Check the status of an audio extraction job",
16
+ responses={
17
+ 200: {"description": "Job status retrieved successfully"},
18
+ 404: {"description": "Job not found"}
19
+ })
20
  async def get_job_status(
21
+ job_id: str = Path(..., description="The job ID returned from the extraction endpoint"),
22
  use_cases: UseCases
23
  ):
24
  """Get the status of a background processing job."""
 
43
  except Exception as e:
44
  raise HTTPException(500, f"Error checking job status: {str(e)}")
45
 
46
+ @router.get("/jobs/{job_id}/download",
47
+ summary="Download Extracted Audio",
48
+ description="Download the audio file from a completed extraction job",
49
+ responses={
50
+ 200: {
51
+ "description": "Audio file",
52
+ "content": {"audio/mpeg": {}, "audio/aac": {}, "audio/wav": {}}
53
+ },
54
+ 400: {"description": "Job not completed"},
55
+ 404: {"description": "Job not found"}
56
+ })
57
  async def download_job_result(
58
+ job_id: str = Path(..., description="The job ID of the completed extraction"),
59
  use_cases: UseCases
60
  ):
61
  """Download the result of a completed job."""
interfaces/web/__init__.py DELETED
@@ -1 +0,0 @@
1
- """Web interface components."""
 
 
interfaces/web/static/script.js DELETED
@@ -1,186 +0,0 @@
1
- class AudioExtractor {
2
- constructor() {
3
- this.form = document.getElementById('extractionForm');
4
- this.fileInput = document.getElementById('videoFile');
5
- this.fileLabel = document.querySelector('.file-input-label');
6
- this.fileInfo = document.getElementById('fileInfo');
7
- this.submitBtn = document.getElementById('submitBtn');
8
- this.resultSection = document.getElementById('resultSection');
9
-
10
- this.initializeEventListeners();
11
- }
12
-
13
- initializeEventListeners() {
14
- this.form.addEventListener('submit', (e) => this.handleSubmit(e));
15
- this.fileInput.addEventListener('change', (e) => this.handleFileSelect(e));
16
-
17
- // Drag and drop
18
- this.fileLabel.addEventListener('dragover', (e) => this.handleDragOver(e));
19
- this.fileLabel.addEventListener('dragleave', (e) => this.handleDragLeave(e));
20
- this.fileLabel.addEventListener('drop', (e) => this.handleDrop(e));
21
- }
22
-
23
- handleFileSelect(e) {
24
- const file = e.target.files[0];
25
- if (file) {
26
- this.displayFileInfo(file);
27
- }
28
- }
29
-
30
- handleDragOver(e) {
31
- e.preventDefault();
32
- this.fileLabel.classList.add('drag-over');
33
- }
34
-
35
- handleDragLeave(e) {
36
- e.preventDefault();
37
- this.fileLabel.classList.remove('drag-over');
38
- }
39
-
40
- handleDrop(e) {
41
- e.preventDefault();
42
- this.fileLabel.classList.remove('drag-over');
43
-
44
- const files = e.dataTransfer.files;
45
- if (files.length > 0) {
46
- this.fileInput.files = files;
47
- this.displayFileInfo(files[0]);
48
- }
49
- }
50
-
51
- displayFileInfo(file) {
52
- const sizeMB = (file.size / (1024 * 1024)).toFixed(2);
53
- this.fileInfo.textContent = `${file.name} (${sizeMB} MB)`;
54
- this.fileLabel.classList.add('has-file');
55
- this.fileLabel.querySelector('span').textContent = 'Change file';
56
- }
57
-
58
- async handleSubmit(e) {
59
- e.preventDefault();
60
-
61
- const file = this.fileInput.files[0];
62
- if (!file) return;
63
-
64
- const formData = new FormData();
65
- formData.append('video', file);
66
- formData.append('output_format', document.getElementById('outputFormat').value);
67
- formData.append('quality', document.getElementById('quality').value);
68
-
69
- this.setLoading(true);
70
- this.showStatus('loading', 'Processing your video...');
71
-
72
- try {
73
- const response = await fetch('/api/v1/extract', {
74
- method: 'POST',
75
- body: formData
76
- });
77
-
78
- if (response.headers.get('content-type')?.includes('audio')) {
79
- // Direct file response
80
- await this.handleDirectResponse(response);
81
- } else {
82
- // Job response
83
- const data = await response.json();
84
- if (response.status === 202) {
85
- await this.handleJobResponse(data);
86
- } else {
87
- throw new Error(data.error || 'Unknown error');
88
- }
89
- }
90
- } catch (error) {
91
- this.showStatus('error', `Error: ${error.message}`);
92
- } finally {
93
- this.setLoading(false);
94
- }
95
- }
96
-
97
- async handleDirectResponse(response) {
98
- const blob = await response.blob();
99
- const url = URL.createObjectURL(blob);
100
- const filename = this.extractFilename(response);
101
-
102
- this.showResult({
103
- type: 'direct',
104
- url: url,
105
- filename: filename
106
- });
107
- }
108
-
109
- async handleJobResponse(data) {
110
- this.showStatus('loading', `Processing large file (${data.file_size_mb.toFixed(1)} MB)...`);
111
-
112
- const checkInterval = setInterval(async () => {
113
- try {
114
- const response = await fetch(`/api/v1/jobs/${data.job_id}`);
115
- const status = await response.json();
116
-
117
- if (status.status === 'completed') {
118
- clearInterval(checkInterval);
119
- this.showResult({
120
- type: 'job',
121
- downloadUrl: status.download_url,
122
- filename: status.filename
123
- });
124
- } else if (status.status === 'failed') {
125
- clearInterval(checkInterval);
126
- this.showStatus('error', `Processing failed: ${status.error}`);
127
- }
128
- } catch (error) {
129
- clearInterval(checkInterval);
130
- this.showStatus('error', `Error checking status: ${error.message}`);
131
- }
132
- }, 2000);
133
- }
134
-
135
- showStatus(type, message) {
136
- this.resultSection.style.display = 'block';
137
- this.resultSection.innerHTML = `
138
- <div class="result-card">
139
- <div class="status-message ${type}">
140
- ${type === 'loading' ? '<span class="spinner"></span>' : ''}
141
- <span>${message}</span>
142
- </div>
143
- </div>
144
- `;
145
- }
146
-
147
- showResult(result) {
148
- const format = document.getElementById('outputFormat').value;
149
-
150
- this.resultSection.style.display = 'block';
151
- this.resultSection.innerHTML = `
152
- <div class="result-card">
153
- <div class="status-message success">
154
- <span>✅ Audio extracted successfully!</span>
155
- </div>
156
- ${result.type === 'direct' ? `
157
- <audio controls src="${result.url}"></audio>
158
- ` : ''}
159
- <a href="${result.type === 'direct' ? result.url : result.downloadUrl}"
160
- download="${result.filename || `extracted.${format}`}"
161
- class="download-btn">
162
- 📥 Download Audio
163
- </a>
164
- </div>
165
- `;
166
- }
167
-
168
- setLoading(loading) {
169
- this.submitBtn.disabled = loading;
170
- this.submitBtn.textContent = loading ? 'Processing...' : 'Extract Audio';
171
- }
172
-
173
- extractFilename(response) {
174
- const disposition = response.headers.get('content-disposition');
175
- if (disposition) {
176
- const match = disposition.match(/filename="(.+)"/);
177
- if (match) return match[1];
178
- }
179
- return 'extracted_audio';
180
- }
181
- }
182
-
183
- // Initialize the app
184
- document.addEventListener('DOMContentLoaded', () => {
185
- new AudioExtractor();
186
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
interfaces/web/static/style.css DELETED
@@ -1,250 +0,0 @@
1
- :root {
2
- --primary-color: #3b82f6;
3
- --primary-hover: #2563eb;
4
- --success-color: #10b981;
5
- --error-color: #ef4444;
6
- --warning-color: #f59e0b;
7
- --bg-color: #f3f4f6;
8
- --card-bg: #ffffff;
9
- --text-primary: #111827;
10
- --text-secondary: #6b7280;
11
- --border-color: #e5e7eb;
12
- }
13
-
14
- * {
15
- margin: 0;
16
- padding: 0;
17
- box-sizing: border-box;
18
- }
19
-
20
- body {
21
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
22
- background-color: var(--bg-color);
23
- color: var(--text-primary);
24
- line-height: 1.6;
25
- }
26
-
27
- .container {
28
- max-width: 800px;
29
- margin: 0 auto;
30
- padding: 2rem;
31
- }
32
-
33
- header {
34
- text-align: center;
35
- margin-bottom: 3rem;
36
- }
37
-
38
- header h1 {
39
- font-size: 2.5rem;
40
- margin-bottom: 0.5rem;
41
- }
42
-
43
- header p {
44
- color: var(--text-secondary);
45
- font-size: 1.125rem;
46
- }
47
-
48
- .upload-section {
49
- background: var(--card-bg);
50
- border-radius: 1rem;
51
- padding: 2rem;
52
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
53
- }
54
-
55
- .file-input-wrapper {
56
- margin-bottom: 2rem;
57
- }
58
-
59
- #videoFile {
60
- display: none;
61
- }
62
-
63
- .file-input-label {
64
- display: flex;
65
- flex-direction: column;
66
- align-items: center;
67
- padding: 3rem;
68
- border: 2px dashed var(--border-color);
69
- border-radius: 0.5rem;
70
- cursor: pointer;
71
- transition: all 0.3s;
72
- }
73
-
74
- .file-input-label:hover {
75
- border-color: var(--primary-color);
76
- background-color: #f9fafb;
77
- }
78
-
79
- .file-input-label.has-file {
80
- border-color: var(--success-color);
81
- background-color: #f0fdf4;
82
- }
83
-
84
- .upload-icon {
85
- width: 48px;
86
- height: 48px;
87
- fill: var(--text-secondary);
88
- margin-bottom: 1rem;
89
- }
90
-
91
- .file-info {
92
- margin-top: 1rem;
93
- text-align: center;
94
- color: var(--text-secondary);
95
- }
96
-
97
- .options-grid {
98
- display: grid;
99
- grid-template-columns: 1fr 1fr;
100
- gap: 1.5rem;
101
- margin-bottom: 2rem;
102
- }
103
-
104
- .form-group {
105
- display: flex;
106
- flex-direction: column;
107
- }
108
-
109
- .form-group label {
110
- font-weight: 500;
111
- margin-bottom: 0.5rem;
112
- color: var(--text-primary);
113
- }
114
-
115
- select {
116
- padding: 0.75rem;
117
- border: 1px solid var(--border-color);
118
- border-radius: 0.375rem;
119
- background-color: white;
120
- font-size: 1rem;
121
- transition: border-color 0.3s;
122
- }
123
-
124
- select:focus {
125
- outline: none;
126
- border-color: var(--primary-color);
127
- box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
128
- }
129
-
130
- .submit-btn {
131
- width: 100%;
132
- padding: 1rem;
133
- background-color: var(--primary-color);
134
- color: white;
135
- border: none;
136
- border-radius: 0.5rem;
137
- font-size: 1.125rem;
138
- font-weight: 600;
139
- cursor: pointer;
140
- transition: background-color 0.3s;
141
- }
142
-
143
- .submit-btn:hover:not(:disabled) {
144
- background-color: var(--primary-hover);
145
- }
146
-
147
- .submit-btn:disabled {
148
- background-color: var(--text-secondary);
149
- cursor: not-allowed;
150
- }
151
-
152
- .result-section {
153
- margin-top: 2rem;
154
- display: none;
155
- }
156
-
157
- .result-card {
158
- background: var(--card-bg);
159
- border-radius: 1rem;
160
- padding: 2rem;
161
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
162
- }
163
-
164
- .status-message {
165
- display: flex;
166
- align-items: center;
167
- margin-bottom: 1.5rem;
168
- padding: 1rem;
169
- border-radius: 0.5rem;
170
- }
171
-
172
- .status-message.success {
173
- background-color: #f0fdf4;
174
- color: var(--success-color);
175
- }
176
-
177
- .status-message.error {
178
- background-color: #fef2f2;
179
- color: var(--error-color);
180
- }
181
-
182
- .status-message.loading {
183
- background-color: #fefce8;
184
- color: var(--warning-color);
185
- }
186
-
187
- .spinner {
188
- display: inline-block;
189
- width: 20px;
190
- height: 20px;
191
- border: 3px solid rgba(0, 0, 0, 0.1);
192
- border-radius: 50%;
193
- border-top-color: currentColor;
194
- animation: spin 1s ease-in-out infinite;
195
- margin-right: 0.75rem;
196
- }
197
-
198
- @keyframes spin {
199
- to { transform: rotate(360deg); }
200
- }
201
-
202
- audio {
203
- width: 100%;
204
- margin: 1rem 0;
205
- }
206
-
207
- .download-btn {
208
- display: inline-flex;
209
- align-items: center;
210
- padding: 0.75rem 1.5rem;
211
- background-color: var(--success-color);
212
- color: white;
213
- text-decoration: none;
214
- border-radius: 0.375rem;
215
- font-weight: 500;
216
- transition: background-color 0.3s;
217
- }
218
-
219
- .download-btn:hover {
220
- background-color: #059669;
221
- }
222
-
223
- footer {
224
- text-align: center;
225
- margin-top: 4rem;
226
- color: var(--text-secondary);
227
- }
228
-
229
- footer a {
230
- color: var(--primary-color);
231
- text-decoration: none;
232
- }
233
-
234
- footer a:hover {
235
- text-decoration: underline;
236
- }
237
-
238
- /* Responsive */
239
- @media (max-width: 640px) {
240
- .container {
241
- padding: 1rem;
242
- }
243
-
244
- header h1 {
245
- font-size: 2rem;
246
- }
247
-
248
- .options-grid {
249
- grid-template-columns: 1fr;
250
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
interfaces/web/templates/index.html DELETED
@@ -1,69 +0,0 @@
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>Video to Audio Extractor</title>
7
- <link rel="stylesheet" href="/static/style.css">
8
- </head>
9
- <body>
10
- <div class="container">
11
- <header>
12
- <h1>🎵 Video to Audio Extractor</h1>
13
- <p>Convert your videos to high-quality audio files</p>
14
- </header>
15
-
16
- <main>
17
- <div class="upload-section">
18
- <form id="extractionForm">
19
- <div class="file-input-wrapper">
20
- <input type="file" id="videoFile" accept="video/*" required>
21
- <label for="videoFile" class="file-input-label">
22
- <svg class="upload-icon" viewBox="0 0 24 24">
23
- <path d="M9 16h6v-6h4l-7-7-7 7h4v6zm-4 2h14v2H5v-2z"/>
24
- </svg>
25
- <span>Choose video file or drag and drop</span>
26
- </label>
27
- <div class="file-info" id="fileInfo"></div>
28
- </div>
29
-
30
- <div class="options-grid">
31
- <div class="form-group">
32
- <label for="outputFormat">Output Format</label>
33
- <select id="outputFormat" required>
34
- <option value="mp3" selected>MP3 - Most Compatible</option>
35
- <option value="aac">AAC - Better Quality</option>
36
- <option value="wav">WAV - Uncompressed</option>
37
- <option value="flac">FLAC - Lossless</option>
38
- <option value="m4a">M4A - Apple Format</option>
39
- <option value="ogg">OGG - Open Format</option>
40
- </select>
41
- </div>
42
-
43
- <div class="form-group">
44
- <label for="quality">Quality</label>
45
- <select id="quality" required>
46
- <option value="high">High - Best Quality</option>
47
- <option value="medium" selected>Medium - Balanced</option>
48
- <option value="low">Low - Smaller Size</option>
49
- </select>
50
- </div>
51
- </div>
52
-
53
- <button type="submit" class="submit-btn" id="submitBtn">
54
- Extract Audio
55
- </button>
56
- </form>
57
- </div>
58
-
59
- <div class="result-section" id="resultSection"></div>
60
- </main>
61
-
62
- <footer>
63
- <p>Powered by FFmpeg | <a href="/api/v1/info">API Documentation</a></p>
64
- </footer>
65
- </div>
66
-
67
- <script src="/static/script.js"></script>
68
- </body>
69
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt CHANGED
@@ -10,8 +10,5 @@ pydantic-settings==2.1.0
10
  aiofiles==23.2.1
11
  ffmpeg-python==0.2.0
12
 
13
- # Web Interface
14
- jinja2==3.1.2
15
-
16
  # Utilities
17
  python-dotenv==1.0.0
 
10
  aiofiles==23.2.1
11
  ffmpeg-python==0.2.0
12
 
 
 
 
13
  # Utilities
14
  python-dotenv==1.0.0