This commit is contained in:
Saufi
2026-06-02 17:35:45 +08:00
commit 4ef99b1f81
148 changed files with 21134 additions and 0 deletions

40
docker/nginx/default.conf Normal file
View 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;
}
}

View 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

View 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
View 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
View 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

View 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

View 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"]

View 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

View 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