Skip to content

Commit bbb63b4

Browse files
committed
changes
1 parent fc932d8 commit bbb63b4

File tree

6 files changed

+216
-0
lines changed

6 files changed

+216
-0
lines changed

app/engine.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from fastapi import HTTPException
2+
from app.utils.ffmpeg_client import run_ffmpeg_command
3+
from app.config import settings
4+
5+
6+
def transcode(input_path, output_path, codec, crf, preset):
7+
cmd = ['-y', '-i', input_path, '-c:v', codec, '-preset', preset, '-crf', str(crf), output_path]
8+
res = run_ffmpeg_command(cmd)
9+
if res.returncode != 0:
10+
raise HTTPException(status_code=500, detail=res.stderr.decode())
11+
return output_path
12+
13+
14+
def measure_quality(reference_path, distorted_path, metrics: list):
15+
result = {}
16+
if 'vmaf' in metrics:
17+
cmd = ['-i', distorted_path, '-i', reference_path, '-lavfi', f"libvmaf=model_path={settings.VMAF_PATH}", '-f', 'null', '-']
18+
r = run_ffmpeg_command(cmd)
19+
if r.returncode != 0:
20+
raise HTTPException(status_code=500, detail=r.stderr.decode())
21+
result['vmaf'] = r.stderr.decode()
22+
if 'psnr' in metrics:
23+
cmd = ['-i', distorted_path, '-i', reference_path, '-lavfi', 'psnr', '-f', 'null', '-']
24+
r = run_ffmpeg_command(cmd)
25+
result['psnr'] = r.stderr.decode()
26+
if 'ssim' in metrics:
27+
cmd = ['-i', distorted_path, '-i', reference_path, '-lavfi', 'ssim', '-f', 'null', '-']
28+
r = run_ffmpeg_command(cmd)
29+
result['ssim'] = r.stderr.decode()
30+
return result
31+
32+
33+
def process_image(req, input_path, output_path):
34+
filters = []
35+
for op in req.operations:
36+
if op.type == 'resize':
37+
filters.append(f"scale={op.params['width']}:{op.params['height']}")
38+
if op.type == 'crop':
39+
filters.append(f"crop={op.params['width']}:{op.params['height']}:{op.params.get('x',0)}:{op.params.get('y',0)}")
40+
if op.type == 'filter':
41+
filters.append(f"{op.params['name']}={op.params.get('args','')}")
42+
if op.type == 'watermark':
43+
filters.append(f"movie={op.params['file']}[wm];[in][wm]overlay={op.params.get('x',10)}:{op.params.get('y',10)}")
44+
cmd = ['-y', '-i', input_path, '-vf', ','.join(filters), output_path]
45+
res = run_ffmpeg_command(cmd)
46+
if res.returncode != 0:
47+
raise HTTPException(status_code=500, detail=res.stderr.decode())
48+
return output_path
49+
50+
51+
def convert_audio(req, input_path, output_path):
52+
cmd = ['-y', '-i', input_path, '-c:a', req.target_codec, '-b:a', req.bitrate, '-ar', str(req.sample_rate), '-ac', str(req.channels), output_path]
53+
res = run_ffmpeg_command(cmd)
54+
if res.returncode != 0:
55+
raise HTTPException(status_code=500, detail=res.stderr.decode())
56+
return output_path

app/routers/audio.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
2+
from app.schemas import ImageReq, JobResp, JobStatusResp
3+
from app.engine import process_image
4+
from app.jobs import jobs, Job, JobStatus
5+
from app.auth import get_current_user
6+
from app.utils.s3_utils import fetch, upload
7+
import uuid, time
8+
9+
router = APIRouter(prefix="/api/v1/image", dependencies=[Depends(get_current_user)])
10+
11+
@router.get("/health")
12+
def health(): return {"status":"ok"}
13+
14+
@router.post("/process", response_model=JobResp)
15+
def api_process_image(req: ImageReq, background_tasks: BackgroundTasks):
16+
job_id=uuid.uuid4().hex; job=Job(job_id); jobs[job_id]=job
17+
def run_job():
18+
job.status=JobStatus.RUNNING; job.start_time=time.time()
19+
try:
20+
in_path=fetch(req.input)
21+
out_path=req.output.local_path or f"/tmp/{job_id}_out.png"
22+
process_image(req, in_path, out_path)
23+
upload(out_path, req.output)
24+
job.log=f"Image processed to {out_path}"; job.status=JobStatus.SUCCESS
25+
except Exception as e:
26+
job.error=str(e); job.status=JobStatus.FAILED
27+
finally:
28+
job.end_time=time.time()
29+
background_tasks.add_task(run_job)
30+
return {"job_id":job_id}
31+
32+
@router.get("/jobs/{job_id}", response_model=JobStatusResp)
33+
def get_image_job(job_id):
34+
job=jobs.get(job_id)
35+
if not job: raise HTTPException(404)
36+
return job.to_dict()

app/routers/auth.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from fastapi import APIRouter, Depends, HTTPException
2+
from fastapi.security import OAuth2PasswordRequestForm
3+
from app.auth import authenticate_user, create_access_token
4+
from app.schemas import Token
5+
6+
router = APIRouter(prefix="/api/v1/auth")
7+
8+
@router.post("/token", response_model=Token)
9+
def login(form_data: OAuth2PasswordRequestForm = Depends()):
10+
user = authenticate_user(form_data.username, form_data.password)
11+
if not user:
12+
raise HTTPException(status_code=400, detail="Incorrect username or password")
13+
access_token = create_access_token(data={"sub": user["username"]})
14+
return {"access_token": access_token, "token_type": "bearer"}
15+
16+
17+
18+
19+
NEW
20+

app/routers/image.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
2+
from app.schemas import ImageReq, JobResp, JobStatusResp
3+
from app.engine import process_image
4+
from app.jobs import jobs, Job, JobStatus
5+
from app.auth import get_current_user
6+
from app.utils.s3_utils import fetch, upload
7+
import uuid, time
8+
9+
router = APIRouter(prefix="/api/v1/image", dependencies=[Depends(get_current_user)])
10+
11+
@router.get("/health")
12+
def health(): return {"status":"ok"}
13+
14+
@router.post("/process", response_model=JobResp)
15+
def api_process_image(req: ImageReq, background_tasks: BackgroundTasks):
16+
job_id=uuid.uuid4().hex; job=Job(job_id); jobs[job_id]=job
17+
def run_job():
18+
job.status=JobStatus.RUNNING; job.start_time=time.time()
19+
try:
20+
in_path=fetch(req.input)
21+
out_path=req.output.local_path or f"/tmp/{job_id}_out.png"
22+
process_image(req, in_path, out_path)
23+
upload(out_path, req.output)
24+
job.log=f"Image processed to {out_path}"; job.status=JobStatus.SUCCESS
25+
except Exception as e:
26+
job.error=str(e); job.status=JobStatus.FAILED
27+
finally:
28+
job.end_time=time.time()
29+
background_tasks.add_task(run_job)
30+
return {"job_id":job_id}
31+
32+
@router.get("/jobs/{job_id}", response_model=JobStatusResp)
33+
def get_image_job(job_id):
34+
job=jobs.get(job_id)
35+
if not job: raise HTTPException(404)
36+
return job.to_dict()

app/routers/video.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
2+
from app.schemas import TranscodeReq, QualityReq, JobResp, JobStatusResp
3+
from app.engine import transcode, measure_quality
4+
from app.jobs import jobs, Job, JobStatus
5+
from app.auth import get_current_user
6+
from app.utils.s3_utils import fetch, upload
7+
import uuid, time
8+
9+
router = APIRouter(prefix="/api/v1/video", dependencies=[Depends(get_current_user)])
10+
11+
@router.get("/health")
12+
def health():
13+
return {"status": "ok"}
14+
15+
@router.post("/transcode", response_model=JobResp)
16+
def api_transcode(req: TranscodeReq, background_tasks: BackgroundTasks):
17+
job_id = uuid.uuid4().hex
18+
job = Job(job_id); jobs[job_id]=job
19+
def run_job():
20+
job.status = JobStatus.RUNNING; job.start_time = time.time()
21+
try:
22+
in_path = fetch(req.input)
23+
out_path = req.output.local_path or f"/tmp/{job_id}_out.{req.output.s3_path.split('.')[-1]}"
24+
transcode(in_path, out_path, req.codec, req.crf, req.preset)
25+
upload(out_path, req.output)
26+
job.log = f"Encoded to {out_path}"; job.status = JobStatus.SUCCESS
27+
except Exception as e:
28+
job.error = str(e); job.status = JobStatus.FAILED
29+
finally:
30+
job.end_time = time.time()
31+
background_tasks.add_task(run_job)
32+
return {"job_id": job_id}
33+
34+
@router.get("/jobs/{job_id}", response_model=JobStatusResp)
35+
def get_job(job_id: str):
36+
job = jobs.get(job_id)
37+
if not job: raise HTTPException(404, "Job not found")
38+
return job.to_dict()
39+
40+
@router.post("/quality")
41+
def api_quality(req: QualityReq):
42+
ref = fetch(req.reference); dist = fetch(req.distorted)
43+
return measure_quality(ref, dist, req.metrics)

app/utils/s3_utils.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import boto3
2+
import os
3+
from urllib.parse import urlparse
4+
from app.config import settings
5+
6+
s3 = boto3.client('s3',
7+
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
8+
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
9+
region_name=settings.AWS_REGION
10+
)
11+
12+
def fetch(loc):
13+
if loc.local_path:
14+
return loc.local_path
15+
if loc.s3_path:
16+
p = urlparse(loc.s3_path)
17+
out = f"/tmp/{os.path.basename(p.path)}"
18+
s3.download_file(p.netloc, p.path.lstrip('/'), out)
19+
return out
20+
raise ValueError("Invalid location")
21+
22+
def upload(local, loc):
23+
if loc.s3_path:
24+
p = urlparse(loc.s3_path)
25+
s3.upload_file(local, p.netloc, p.path.lstrip('/'))

0 commit comments

Comments
 (0)