first
This commit is contained in:
40
docker/nginx/default.conf
Normal file
40
docker/nginx/default.conf
Normal file
@@ -0,0 +1,40 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
root /var/www/html/public;
|
||||
index index.php index.html;
|
||||
|
||||
client_max_body_size 210M;
|
||||
|
||||
add_header X-Frame-Options "DENY";
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
add_header X-Content-Type-Options "nosniff";
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin";
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
location = /favicon.ico { access_log off; log_not_found off; }
|
||||
location = /robots.txt { access_log off; log_not_found off; }
|
||||
|
||||
error_page 404 /index.php;
|
||||
|
||||
location ~ \.php$ {
|
||||
fastcgi_pass app:9000;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
|
||||
include fastcgi_params;
|
||||
fastcgi_read_timeout 300;
|
||||
}
|
||||
|
||||
# Deny access to hidden files
|
||||
location ~ /\. {
|
||||
deny all;
|
||||
}
|
||||
|
||||
# Deny direct access to storage (private files served via PHP)
|
||||
location ~ ^/storage/ {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
25
docker/php-apache/Dockerfile
Normal file
25
docker/php-apache/Dockerfile
Normal file
@@ -0,0 +1,25 @@
|
||||
FROM php:8.5-apache
|
||||
|
||||
ENV APACHE_DOCUMENT_ROOT=/var/www/html/public
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
git \
|
||||
unzip \
|
||||
libzip-dev \
|
||||
libpng-dev \
|
||||
libjpeg62-turbo-dev \
|
||||
libfreetype6-dev \
|
||||
libicu-dev \
|
||||
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
|
||||
&& docker-php-ext-install pdo_mysql gd intl zip \
|
||||
&& pecl install redis \
|
||||
&& docker-php-ext-enable redis \
|
||||
&& a2enmod rewrite headers \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||
COPY docker/php-apache/vhost.conf /etc/apache2/sites-available/000-default.conf
|
||||
|
||||
WORKDIR /var/www/html
|
||||
|
||||
RUN chown -R www-data:www-data /var/www/html
|
||||
13
docker/php-apache/vhost.conf
Normal file
13
docker/php-apache/vhost.conf
Normal file
@@ -0,0 +1,13 @@
|
||||
<VirtualHost *:80>
|
||||
ServerAdmin webmaster@localhost
|
||||
DocumentRoot /var/www/html/public
|
||||
|
||||
<Directory /var/www/html/public>
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
Options -Indexes
|
||||
</Directory>
|
||||
|
||||
ErrorLog ${APACHE_LOG_DIR}/error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/access.log combined
|
||||
</VirtualHost>
|
||||
4
docker/php/custom.ini
Normal file
4
docker/php/custom.ini
Normal file
@@ -0,0 +1,4 @@
|
||||
display_errors = On
|
||||
display_startup_errors = Off
|
||||
log_errors = On
|
||||
error_reporting = E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED
|
||||
34
docker/php/php.ini
Normal file
34
docker/php/php.ini
Normal file
@@ -0,0 +1,34 @@
|
||||
; Speech2Text MBIP — PHP Configuration
|
||||
|
||||
[PHP]
|
||||
upload_max_filesize = 210M
|
||||
post_max_size = 215M
|
||||
max_execution_time = 300
|
||||
max_input_time = 300
|
||||
memory_limit = 512M
|
||||
|
||||
; Security
|
||||
expose_php = Off
|
||||
display_errors = Off
|
||||
log_errors = On
|
||||
error_log = /var/log/php_errors.log
|
||||
|
||||
; File uploads
|
||||
file_uploads = On
|
||||
upload_tmp_dir = /tmp
|
||||
|
||||
; Session
|
||||
session.cookie_httponly = 1
|
||||
session.cookie_secure = 1
|
||||
session.use_strict_mode = 1
|
||||
session.gc_maxlifetime = 7200
|
||||
|
||||
; Timezone
|
||||
date.timezone = Asia/Kuala_Lumpur
|
||||
|
||||
[opcache]
|
||||
opcache.enable = 1
|
||||
opcache.memory_consumption = 128
|
||||
opcache.max_accelerated_files = 10000
|
||||
opcache.revalidate_freq = 0
|
||||
opcache.validate_timestamps = 0
|
||||
27
docker/supervisor/supervisord.conf
Normal file
27
docker/supervisor/supervisord.conf
Normal file
@@ -0,0 +1,27 @@
|
||||
[supervisord]
|
||||
nodaemon=true
|
||||
user=root
|
||||
logfile=/var/log/supervisor/supervisord.log
|
||||
pidfile=/var/run/supervisord.pid
|
||||
|
||||
[program:php-fpm]
|
||||
command=php-fpm
|
||||
autostart=true
|
||||
autorestart=true
|
||||
priority=5
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
|
||||
[program:queue-worker]
|
||||
process_name=%(program_name)s_%(process_num)02d
|
||||
command=php /var/www/html/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600 --memory=256
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stopasgroup=true
|
||||
killasgroup=true
|
||||
numprocs=2
|
||||
redirect_stderr=true
|
||||
stdout_logfile=/var/log/supervisor/queue-worker.log
|
||||
stopwaitsecs=3600
|
||||
18
docker/transcription-worker/Dockerfile
Normal file
18
docker/transcription-worker/Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies for faster-whisper
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ffmpeg \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Python dependencies
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY main.py .
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"]
|
||||
125
docker/transcription-worker/main.py
Normal file
125
docker/transcription-worker/main.py
Normal file
@@ -0,0 +1,125 @@
|
||||
"""
|
||||
Speech2Text MBIP — Transcription Worker
|
||||
FastAPI service wrapping faster-whisper for Bahasa Melayu transcription.
|
||||
Internal-only service; not exposed to internet.
|
||||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import FastAPI, UploadFile, File, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from faster_whisper import WhisperModel
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = FastAPI(title="Speech2Text Transcription Worker", docs_url=None, redoc_url=None)
|
||||
|
||||
# Config from environment
|
||||
MODEL_SIZE = os.getenv("WHISPER_MODEL", "small")
|
||||
LANGUAGE = os.getenv("WHISPER_LANGUAGE", "ms")
|
||||
DEVICE = os.getenv("WHISPER_DEVICE", "cpu")
|
||||
COMPUTE_TYPE = os.getenv("WHISPER_COMPUTE_TYPE", "int8")
|
||||
|
||||
logger.info(f"Loading Whisper model: {MODEL_SIZE}, device: {DEVICE}, compute: {COMPUTE_TYPE}")
|
||||
|
||||
# Load model once at startup
|
||||
model = WhisperModel(MODEL_SIZE, device=DEVICE, compute_type=COMPUTE_TYPE)
|
||||
|
||||
logger.info("Whisper model loaded successfully.")
|
||||
|
||||
|
||||
class TranscribeResponse(BaseModel):
|
||||
success: bool
|
||||
transcript: Optional[str] = None
|
||||
language: Optional[str] = None
|
||||
confidence: Optional[float] = None
|
||||
duration_seconds: Optional[float] = None
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health_check():
|
||||
return {"status": "ok", "model": MODEL_SIZE, "language": LANGUAGE}
|
||||
|
||||
|
||||
@app.post("/transcribe", response_model=TranscribeResponse)
|
||||
async def transcribe_audio(audio: UploadFile = File(...)):
|
||||
"""
|
||||
Terima fail audio, transkripkan ke teks Bahasa Melayu.
|
||||
Hanya accessible dari dalam Docker network sahaja.
|
||||
"""
|
||||
# Validate content type
|
||||
allowed_types = {
|
||||
"audio/mpeg", "audio/mp3", "audio/wav", "audio/x-wav",
|
||||
"audio/mp4", "audio/x-m4a", "audio/aac", "video/mp4",
|
||||
"audio/ogg", "audio/flac", "audio/webm",
|
||||
}
|
||||
|
||||
if audio.content_type and audio.content_type not in allowed_types:
|
||||
logger.warning(f"Rejected content type: {audio.content_type}")
|
||||
raise HTTPException(status_code=400, detail=f"Unsupported audio format: {audio.content_type}")
|
||||
|
||||
# Save to temp file
|
||||
suffix = Path(audio.filename or "audio.mp3").suffix or ".mp3"
|
||||
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
|
||||
content = await audio.read()
|
||||
tmp.write(content)
|
||||
tmp_path = tmp.name
|
||||
|
||||
logger.info(f"Transcribing: {audio.filename}, size: {len(content)} bytes")
|
||||
|
||||
# Run transcription
|
||||
segments, info = model.transcribe(
|
||||
tmp_path,
|
||||
language=LANGUAGE,
|
||||
beam_size=5,
|
||||
vad_filter=True,
|
||||
)
|
||||
|
||||
# Collect segments
|
||||
all_text = []
|
||||
total_confidence = []
|
||||
|
||||
for segment in segments:
|
||||
all_text.append(segment.text.strip())
|
||||
# faster-whisper segments have avg_logprob; convert to 0-1 range
|
||||
if hasattr(segment, "avg_logprob") and segment.avg_logprob is not None:
|
||||
import math
|
||||
confidence = min(1.0, max(0.0, math.exp(segment.avg_logprob)))
|
||||
total_confidence.append(confidence)
|
||||
|
||||
transcript = " ".join(all_text).strip()
|
||||
avg_confidence = sum(total_confidence) / len(total_confidence) if total_confidence else None
|
||||
duration = info.duration if hasattr(info, "duration") else None
|
||||
|
||||
logger.info(f"Transcription complete: {len(transcript)} chars, lang: {info.language}")
|
||||
|
||||
return TranscribeResponse(
|
||||
success=True,
|
||||
transcript=transcript,
|
||||
language=info.language,
|
||||
confidence=round(avg_confidence, 4) if avg_confidence else None,
|
||||
duration_seconds=round(duration, 2) if duration else None,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Transcription failed: {e}", exc_info=True)
|
||||
return TranscribeResponse(
|
||||
success=False,
|
||||
error=str(e),
|
||||
)
|
||||
|
||||
finally:
|
||||
# Always cleanup temp file
|
||||
if "tmp_path" in locals():
|
||||
try:
|
||||
os.unlink(tmp_path)
|
||||
except Exception:
|
||||
pass
|
||||
6
docker/transcription-worker/requirements.txt
Normal file
6
docker/transcription-worker/requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
fastapi==0.115.0
|
||||
uvicorn==0.30.6
|
||||
faster-whisper==1.0.3
|
||||
python-multipart==0.0.9
|
||||
pydantic==2.8.2
|
||||
requests==2.32.3
|
||||
Reference in New Issue
Block a user