Files
bini-shorts-maker/backend/app/routers/jobs.py
kihong.kim c3795138da Initial commit: YouTube Shorts maker application
Features:
- Video download from TikTok/Douyin using yt-dlp
- Audio transcription with OpenAI Whisper
- GPT-4 translation (direct/summarize/rewrite modes)
- Subtitle generation with ASS format
- Video trimming with frame-accurate preview
- BGM integration with volume control
- Intro text overlay support
- Thumbnail generation with text overlay

Tech stack:
- Backend: FastAPI, Python 3.11+
- Frontend: React, Vite, TailwindCSS
- Video processing: FFmpeg
- AI: OpenAI Whisper, GPT-4

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 21:38:34 +09:00

176 lines
5.3 KiB
Python

import os
import shutil
from fastapi import APIRouter, HTTPException
from fastapi.responses import FileResponse
from app.models.schemas import JobInfo
from app.models.job_store import job_store
from app.config import settings
router = APIRouter()
@router.get("/", response_model=list[JobInfo])
async def list_jobs(limit: int = 50):
"""List all jobs."""
return job_store.list_jobs(limit=limit)
@router.get("/{job_id}", response_model=JobInfo)
async def get_job(job_id: str):
"""Get job details."""
job = job_store.get_job(job_id)
if not job:
raise HTTPException(status_code=404, detail="Job not found")
print(f"[API GET] Job {job_id}: status={job.status}, progress={job.progress}")
return job
@router.delete("/{job_id}")
async def delete_job(job_id: str):
"""Delete a job and its files."""
job = job_store.get_job(job_id)
if not job:
raise HTTPException(status_code=404, detail="Job not found")
# Delete associated files
download_dir = os.path.join(settings.DOWNLOAD_DIR, job_id)
processed_dir = os.path.join(settings.PROCESSED_DIR, job_id)
if os.path.exists(download_dir):
shutil.rmtree(download_dir)
if os.path.exists(processed_dir):
shutil.rmtree(processed_dir)
job_store.delete_job(job_id)
return {"message": f"Job {job_id} deleted"}
@router.get("/{job_id}/download")
async def download_output(job_id: str):
"""Download the processed video."""
job = job_store.get_job(job_id)
if not job:
raise HTTPException(status_code=404, detail="Job not found")
if not job.output_path or not os.path.exists(job.output_path):
raise HTTPException(status_code=404, detail="Output file not found")
return FileResponse(
path=job.output_path,
media_type="video/mp4",
filename=f"shorts_{job_id}.mp4"
)
@router.get("/{job_id}/original")
async def download_original(job_id: str):
"""Download the original video."""
job = job_store.get_job(job_id)
if not job:
raise HTTPException(status_code=404, detail="Job not found")
if not job.video_path or not os.path.exists(job.video_path):
raise HTTPException(status_code=404, detail="Original video not found")
filename = os.path.basename(job.video_path)
# Disable caching to ensure trimmed video is always fetched fresh
return FileResponse(
path=job.video_path,
media_type="video/mp4",
filename=filename,
headers={
"Cache-Control": "no-cache, no-store, must-revalidate",
"Pragma": "no-cache",
"Expires": "0"
}
)
@router.get("/{job_id}/subtitle")
async def download_subtitle(job_id: str, format: str = "ass"):
"""Download the subtitle file."""
job = job_store.get_job(job_id)
if not job:
raise HTTPException(status_code=404, detail="Job not found")
if not job.video_path:
raise HTTPException(status_code=404, detail="Video not found")
job_dir = os.path.dirname(job.video_path)
subtitle_path = os.path.join(job_dir, f"subtitle.{format}")
if not os.path.exists(subtitle_path):
# Try to generate from transcript
if job.transcript:
from app.services.transcriber import segments_to_ass, segments_to_srt
if format == "srt":
content = segments_to_srt(job.transcript, use_translated=True)
else:
content = segments_to_ass(job.transcript, use_translated=True)
with open(subtitle_path, "w", encoding="utf-8") as f:
f.write(content)
else:
raise HTTPException(status_code=404, detail="Subtitle not found")
return FileResponse(
path=subtitle_path,
media_type="text/plain",
filename=f"subtitle_{job_id}.{format}"
)
@router.get("/{job_id}/thumbnail")
async def download_thumbnail(job_id: str):
"""Download the generated thumbnail image."""
job = job_store.get_job(job_id)
if not job:
raise HTTPException(status_code=404, detail="Job not found")
# Check for thumbnail in processed directory
thumbnail_path = os.path.join(settings.PROCESSED_DIR, f"{job_id}_thumbnail.jpg")
if not os.path.exists(thumbnail_path):
raise HTTPException(status_code=404, detail="Thumbnail not found. Generate it first using /process/{job_id}/thumbnail")
return FileResponse(
path=thumbnail_path,
media_type="image/jpeg",
filename=f"thumbnail_{job_id}.jpg"
)
@router.post("/{job_id}/re-edit")
async def re_edit_job(job_id: str):
"""Reset job status to awaiting_review for re-editing."""
from app.models.schemas import JobStatus
job = job_store.get_job(job_id)
if not job:
raise HTTPException(status_code=404, detail="Job not found")
if job.status != JobStatus.COMPLETED:
raise HTTPException(
status_code=400,
detail="Only completed jobs can be re-edited"
)
# Check if transcript exists for re-editing
if not job.transcript:
raise HTTPException(
status_code=400,
detail="No transcript found. Cannot re-edit."
)
# Reset status to awaiting_review
job_store.update_job(
job_id,
status=JobStatus.AWAITING_REVIEW,
progress=70,
error=None
)
return {"message": "Job ready for re-editing", "job_id": job_id}