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>
This commit is contained in:
175
backend/app/routers/jobs.py
Normal file
175
backend/app/routers/jobs.py
Normal file
@@ -0,0 +1,175 @@
|
||||
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}
|
||||
Reference in New Issue
Block a user