ismdrobiul489 commited on
Commit
5299ad7
·
1 Parent(s): 6c9ac70

Unify Story Reels API with Video Creator pattern: /story-reel endpoints

Browse files
modules/story_reels/router.py CHANGED
@@ -1,5 +1,6 @@
1
  """
2
  Story Reels Router - API Endpoints
 
3
  """
4
  from fastapi import APIRouter, HTTPException
5
  from fastapi.responses import FileResponse
@@ -9,8 +10,8 @@ from .schemas import (
9
  GenerateVideoRequest,
10
  GenerateVideoResponse,
11
  VideoStatusResponse,
12
- PreviewResponse,
13
- JobStatus
14
  )
15
  from .services.story_creator import StoryCreator
16
 
@@ -29,82 +30,64 @@ def set_story_creator(creator: StoryCreator):
29
  router = APIRouter()
30
 
31
 
32
- @router.post("/generate",
33
  response_model=GenerateVideoResponse,
34
  status_code=201,
35
- summary="Generate story video",
36
- description="Generate a character-consistent story video from script"
37
  )
38
- async def generate_video(request: GenerateVideoRequest):
39
  """
40
- Main video generation endpoint.
41
 
 
42
  - Converts script to speech (TTS)
43
  - Generates captions (Whisper)
44
- - Creates character-consistent images (Cloudflare)
45
  - Composes final video (MoviePy)
46
  """
47
  try:
48
- logger.info(f"Generating video for topic: {request.topic}")
49
 
50
- job_id = story_creator.add_to_queue(
51
  topic=request.topic,
52
  image_style=request.image_style.value,
53
  voice=request.voice
54
  )
55
 
56
  return GenerateVideoResponse(
57
- job_id=job_id,
58
  status=JobStatus.queued,
59
  message="Video generation started"
60
  )
61
 
62
  except Exception as e:
63
- logger.error(f"Error starting generation: {e}", exc_info=True)
64
  raise HTTPException(status_code=400, detail=str(e))
65
 
66
 
67
- @router.get("/status/{job_id}",
68
  response_model=VideoStatusResponse,
69
- summary="Get job status",
70
- description="Check the processing status of a video generation job"
71
  )
72
- async def get_status(job_id: str):
73
- """Get video generation status"""
74
- status = story_creator.get_status(job_id)
75
  return VideoStatusResponse(**status)
76
 
77
 
78
- @router.get("/preview/{job_id}/{scene_id}",
79
- response_model=PreviewResponse,
80
- summary="Get scene preview",
81
- description="Get preview of a generated scene image"
82
- )
83
- async def get_preview(job_id: str, scene_id: int):
84
- """Get scene preview"""
85
- scene = story_creator.get_preview(job_id, scene_id)
86
-
87
- if not scene:
88
- raise HTTPException(status_code=404, detail="Scene not found")
89
-
90
- return PreviewResponse(
91
- scene_id=scene["scene_id"],
92
- image_url=scene["image_path"],
93
- prompt=scene["prompt"]
94
- )
95
-
96
-
97
- @router.get("/download/{job_id}",
98
  summary="Download video",
99
- description="Download the generated video file",
100
  responses={
101
  200: {"description": "Video file", "content": {"video/mp4": {}}},
102
  404: {"description": "Video not found"}
103
  }
104
  )
105
- async def download_video(job_id: str):
106
- """Download generated video"""
107
- video_path = story_creator.get_video_path(job_id)
108
 
109
  if not video_path or not video_path.exists():
110
  raise HTTPException(status_code=404, detail="Video not found")
@@ -112,5 +95,52 @@ async def download_video(job_id: str):
112
  return FileResponse(
113
  video_path,
114
  media_type="video/mp4",
115
- filename=f"story_{job_id}.mp4"
116
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
  Story Reels Router - API Endpoints
3
+ Consistent with Video Creator API pattern
4
  """
5
  from fastapi import APIRouter, HTTPException
6
  from fastapi.responses import FileResponse
 
10
  GenerateVideoRequest,
11
  GenerateVideoResponse,
12
  VideoStatusResponse,
13
+ JobStatus,
14
+ StyleEnum
15
  )
16
  from .services.story_creator import StoryCreator
17
 
 
30
  router = APIRouter()
31
 
32
 
33
+ @router.post("/story-reel",
34
  response_model=GenerateVideoResponse,
35
  status_code=201,
36
+ summary="Create a new story reel",
37
+ description="Create a new AI-generated story video. Returns video_id to track progress."
38
  )
39
+ async def create_story_reel(request: GenerateVideoRequest):
40
  """
41
+ Create a new story reel from topic.
42
 
43
+ - AI generates script from topic
44
  - Converts script to speech (TTS)
45
  - Generates captions (Whisper)
46
+ - Creates AI images (NVIDIA/Cloudflare)
47
  - Composes final video (MoviePy)
48
  """
49
  try:
50
+ logger.info(f"Creating story reel for topic: {request.topic}")
51
 
52
+ video_id = story_creator.add_to_queue(
53
  topic=request.topic,
54
  image_style=request.image_style.value,
55
  voice=request.voice
56
  )
57
 
58
  return GenerateVideoResponse(
59
+ job_id=video_id,
60
  status=JobStatus.queued,
61
  message="Video generation started"
62
  )
63
 
64
  except Exception as e:
65
+ logger.error(f"Error creating story reel: {e}", exc_info=True)
66
  raise HTTPException(status_code=400, detail=str(e))
67
 
68
 
69
+ @router.get("/story-reel/{video_id}/status",
70
  response_model=VideoStatusResponse,
71
+ summary="Get video status",
72
+ description="Check the processing status of a story reel (queued, processing, ready, or failed)"
73
  )
74
+ async def get_story_status(video_id: str):
75
+ """Get the status of a story reel"""
76
+ status = story_creator.get_status(video_id)
77
  return VideoStatusResponse(**status)
78
 
79
 
80
+ @router.get("/story-reel/{video_id}",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  summary="Download video",
82
+ description="Download the generated story reel video file (MP4 format)",
83
  responses={
84
  200: {"description": "Video file", "content": {"video/mp4": {}}},
85
  404: {"description": "Video not found"}
86
  }
87
  )
88
+ async def download_story_reel(video_id: str):
89
+ """Download/stream a story reel"""
90
+ video_path = story_creator.get_video_path(video_id)
91
 
92
  if not video_path or not video_path.exists():
93
  raise HTTPException(status_code=404, detail="Video not found")
 
95
  return FileResponse(
96
  video_path,
97
  media_type="video/mp4",
98
+ filename=f"story_{video_id}.mp4"
99
  )
100
+
101
+
102
+ @router.get("/story-reels",
103
+ summary="List all story reels",
104
+ description="Get a list of all story reels with their current status"
105
+ )
106
+ async def list_story_reels():
107
+ """List all story reels"""
108
+ videos = story_creator.list_all_videos()
109
+ return {"videos": videos}
110
+
111
+
112
+ @router.delete("/story-reel/{video_id}",
113
+ summary="Delete story reel",
114
+ description="Delete a story reel video by its ID"
115
+ )
116
+ async def delete_story_reel(video_id: str):
117
+ """Delete a story reel"""
118
+ try:
119
+ story_creator.delete_video(video_id)
120
+ return {"success": True}
121
+ except Exception as e:
122
+ logger.error(f"Error deleting video: {e}")
123
+ raise HTTPException(status_code=500, detail=str(e))
124
+
125
+
126
+ @router.get("/styles",
127
+ summary="List image styles",
128
+ description="Get all available image generation styles"
129
+ )
130
+ async def get_styles():
131
+ """List available image styles"""
132
+ return [{"value": s.value, "name": s.name.replace("_", " ").title()} for s in StyleEnum]
133
+
134
+
135
+ @router.get("/voices",
136
+ summary="List TTS voices",
137
+ description="Get all available text-to-speech voice options"
138
+ )
139
+ async def get_voices():
140
+ """List available TTS voices"""
141
+ return [
142
+ {"value": "af_heart", "name": "Female - Heart"},
143
+ {"value": "am_adam", "name": "Male - Adam"},
144
+ {"value": "af_bella", "name": "Female - Bella"},
145
+ {"value": "am_michael", "name": "Male - Michael"}
146
+ ]
modules/story_reels/services/story_creator.py CHANGED
@@ -95,6 +95,71 @@ class StoryCreator:
95
 
96
  return job_id
97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  async def process_queue(self):
99
  """Process jobs in queue"""
100
  if self.processing:
 
95
 
96
  return job_id
97
 
98
+ def get_status(self, job_id: str) -> Dict:
99
+ """Get job status by ID"""
100
+ job = self.jobs.get(job_id)
101
+ if not job:
102
+ return {
103
+ "job_id": job_id,
104
+ "status": "not_found",
105
+ "progress": 0,
106
+ "error": "Job not found"
107
+ }
108
+
109
+ return {
110
+ "job_id": job["id"],
111
+ "status": job["status"],
112
+ "progress": job.get("progress", 0),
113
+ "video_url": job.get("video_url"),
114
+ "duration": job.get("duration"),
115
+ "error": job.get("error")
116
+ }
117
+
118
+ def get_video_path(self, job_id: str) -> Path:
119
+ """Get video file path for download"""
120
+ video_path = self.config.videos_dir_path / f"{job_id}.mp4"
121
+ if video_path.exists():
122
+ return video_path
123
+ return None
124
+
125
+ def get_preview(self, job_id: str, scene_id: int) -> Dict:
126
+ """Get scene preview"""
127
+ job = self.jobs.get(job_id)
128
+ if not job or not job.get("scenes"):
129
+ return None
130
+
131
+ for scene in job["scenes"]:
132
+ if scene.get("scene_id") == scene_id:
133
+ return scene
134
+ return None
135
+
136
+ def list_all_videos(self) -> List[Dict]:
137
+ """List all videos with their status"""
138
+ videos = []
139
+ for job_id, job in self.jobs.items():
140
+ videos.append({
141
+ "video_id": job_id,
142
+ "topic": job.get("topic", ""),
143
+ "status": job.get("status", "unknown"),
144
+ "progress": job.get("progress", 0),
145
+ "video_url": job.get("video_url"),
146
+ "created_at": job.get("created_at")
147
+ })
148
+ return videos
149
+
150
+ def delete_video(self, job_id: str) -> bool:
151
+ """Delete a video by ID"""
152
+ # Remove from jobs dict
153
+ if job_id in self.jobs:
154
+ del self.jobs[job_id]
155
+
156
+ # Delete video file
157
+ video_path = self.config.videos_dir_path / f"{job_id}.mp4"
158
+ if video_path.exists():
159
+ video_path.unlink()
160
+
161
+ return True
162
+
163
  async def process_queue(self):
164
  """Process jobs in queue"""
165
  if self.processing:
static/index.html CHANGED
@@ -395,7 +395,7 @@
395
  };
396
 
397
  try {
398
- const res = await fetch('/api/story/generate', {
399
  method: 'POST',
400
  headers: { 'Content-Type': 'application/json' },
401
  body: JSON.stringify(data)
@@ -415,11 +415,11 @@
415
  // Poll status
416
  async function pollStatus(jobId, type) {
417
  const status = document.getElementById(type + 'Status');
418
- const endpoint = type === 'story' ? '/api/story/status/' : '/api/video/short-video/';
419
 
420
  const check = async () => {
421
  try {
422
- const res = await fetch(endpoint + jobId + (type === 'video' ? '/status' : ''));
423
  const data = await res.json();
424
 
425
  const progress = data.progress || 0;
@@ -430,7 +430,7 @@
430
 
431
  if (data.status === 'ready') {
432
  status.className = 'status success';
433
- const downloadUrl = type === 'story' ? `/api/story/download/${jobId}` : `/api/video/short-video/${jobId}`;
434
  status.innerHTML += `<br><a href="${downloadUrl}" class="btn btn-primary" style="margin-top: 1rem; display: inline-block;">📥 Download Video</a>`;
435
  } else if (data.status === 'failed') {
436
  status.className = 'status error';
 
395
  };
396
 
397
  try {
398
+ const res = await fetch('/api/story/story-reel', {
399
  method: 'POST',
400
  headers: { 'Content-Type': 'application/json' },
401
  body: JSON.stringify(data)
 
415
  // Poll status
416
  async function pollStatus(jobId, type) {
417
  const status = document.getElementById(type + 'Status');
418
+ const endpoint = type === 'story' ? '/api/story/story-reel/' : '/api/video/short-video/';
419
 
420
  const check = async () => {
421
  try {
422
+ const res = await fetch(endpoint + jobId + '/status');
423
  const data = await res.json();
424
 
425
  const progress = data.progress || 0;
 
430
 
431
  if (data.status === 'ready') {
432
  status.className = 'status success';
433
+ const downloadUrl = type === 'story' ? `/api/story/story-reel/${jobId}` : `/api/video/short-video/${jobId}`;
434
  status.innerHTML += `<br><a href="${downloadUrl}" class="btn btn-primary" style="margin-top: 1rem; display: inline-block;">📥 Download Video</a>`;
435
  } else if (data.status === 'failed') {
436
  status.className = 'status error';