video-generator.controller.ts 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. /**
  2. * 视频生成模块 - API 路由控制器
  3. */
  4. import Router from '@koa/router';
  5. import {
  6. createVideoProject,
  7. getVideoProjects,
  8. getVideoProject,
  9. updateVideoProject,
  10. deleteVideoProject,
  11. generateVideoForProject,
  12. getGenerateProgress,
  13. getMaterials,
  14. uploadMaterial,
  15. deleteMaterial,
  16. createVideoProjectFromBook,
  17. } from './video-generator.service';
  18. const router = new Router();
  19. // ============ 视频项目管理 ============
  20. /**
  21. * GET /api/video/projects
  22. * 获取视频项目列表
  23. */
  24. router.get('/projects', async (ctx) => {
  25. const query = {
  26. userId: ctx.query.userId ? Number(ctx.query.userId) : undefined,
  27. status: ctx.query.status as any,
  28. page: ctx.query.page ? Number(ctx.query.page) : 1,
  29. pageSize: ctx.query.pageSize ? Number(ctx.query.pageSize) : 10,
  30. };
  31. const result = await getVideoProjects(query);
  32. ctx.body = { success: true, data: result };
  33. });
  34. /**
  35. * POST /api/video/projects
  36. * 创建视频项目
  37. */
  38. router.post('/projects', async (ctx) => {
  39. const body = ctx.request.body as any;
  40. const userId = ctx.state.user?.id;
  41. const project = await createVideoProject(body, userId);
  42. ctx.body = { success: true, data: project };
  43. });
  44. /**
  45. * GET /api/video/projects/:id
  46. * 获取视频项目详情
  47. */
  48. router.get('/projects/:id', async (ctx) => {
  49. const id = Number(ctx.params.id);
  50. const project = await getVideoProject(id);
  51. if (!project) {
  52. ctx.status = 404;
  53. ctx.body = { success: false, error: '项目不存在' };
  54. return;
  55. }
  56. ctx.body = { success: true, data: project };
  57. });
  58. /**
  59. * PUT /api/video/projects/:id
  60. * 更新视频项目
  61. */
  62. router.put('/projects/:id', async (ctx) => {
  63. const id = Number(ctx.params.id);
  64. const body = ctx.request.body as any;
  65. const project = await updateVideoProject(id, body);
  66. if (!project) {
  67. ctx.status = 404;
  68. ctx.body = { success: false, error: '项目不存在' };
  69. return;
  70. }
  71. ctx.body = { success: true, data: project };
  72. });
  73. /**
  74. * DELETE /api/video/projects/:id
  75. * 删除视频项目
  76. */
  77. router.delete('/projects/:id', async (ctx) => {
  78. const id = Number(ctx.params.id);
  79. const success = await deleteVideoProject(id);
  80. if (!success) {
  81. ctx.status = 404;
  82. ctx.body = { success: false, error: '项目不存在' };
  83. return;
  84. }
  85. ctx.body = { success: true };
  86. });
  87. // ============ 视频生成 ============
  88. /**
  89. * POST /api/video/projects/:id/generate
  90. * 开始生成视频
  91. */
  92. router.post('/projects/:id/generate', async (ctx) => {
  93. const id = Number(ctx.params.id);
  94. const result = await generateVideoForProject(id);
  95. if (!result.success) {
  96. ctx.status = 400;
  97. ctx.body = { success: false, error: result.error };
  98. return;
  99. }
  100. ctx.body = {
  101. success: true,
  102. data: {
  103. outputUrl: result.outputUrl,
  104. duration: result.duration,
  105. fileSize: result.fileSize,
  106. },
  107. };
  108. });
  109. /**
  110. * GET /api/video/projects/:id/status
  111. * 获取生成状态
  112. */
  113. router.get('/projects/:id/status', async (ctx) => {
  114. const id = Number(ctx.params.id);
  115. const status = await getGenerateProgress(id);
  116. ctx.body = { success: true, data: status };
  117. });
  118. // ============ 素材管理 ============
  119. /**
  120. * GET /api/video/materials
  121. * 获取素材列表
  122. */
  123. router.get('/materials', async (ctx) => {
  124. const query = {
  125. userId: ctx.query.userId ? Number(ctx.query.userId) : undefined,
  126. type: ctx.query.type as any,
  127. category: ctx.query.category as string,
  128. page: ctx.query.page ? Number(ctx.query.page) : 1,
  129. pageSize: ctx.query.pageSize ? Number(ctx.query.pageSize) : 20,
  130. };
  131. const result = await getMaterials(query);
  132. ctx.body = { success: true, data: result };
  133. });
  134. /**
  135. * POST /api/video/materials/upload
  136. * 上传素材(处理 multipart/form-data 文件上传)
  137. */
  138. router.post('/materials/upload', async (ctx) => {
  139. const userId = ctx.state.user?.id;
  140. // 处理 multipart form data
  141. const body = ctx.request.body as any;
  142. const files = (ctx.request as any).files as any;
  143. // 获取上传的文件
  144. const file = files?.file;
  145. let materialUrl = '';
  146. if (file) {
  147. // 文件已上传,url 就是文件的路径
  148. materialUrl = `/uploads/materials/${file.newFilename || file.filename}`;
  149. } else if (body.url) {
  150. // 如果没有文件,使用传入的url
  151. materialUrl = body.url;
  152. } else {
  153. ctx.status = 400;
  154. ctx.body = { success: false, error: 'No file uploaded' };
  155. return;
  156. }
  157. const material = await uploadMaterial({
  158. type: body.type || 'image',
  159. name: body.name || file?.newFilename || 'unnamed',
  160. url: materialUrl,
  161. thumbnail: body.thumbnail,
  162. tags: body.tags ? JSON.parse(body.tags) : [],
  163. category: body.category,
  164. duration: body.duration ? Number(body.duration) : undefined,
  165. size: body.size ? Number(body.size) : undefined,
  166. width: body.width ? Number(body.width) : undefined,
  167. height: body.height ? Number(body.height) : undefined,
  168. }, userId);
  169. ctx.body = { success: true, data: material };
  170. });
  171. /**
  172. * DELETE /api/video/materials/:id
  173. * 删除素材
  174. */
  175. router.delete('/materials/:id', async (ctx) => {
  176. const id = Number(ctx.params.id);
  177. const success = await deleteMaterial(id);
  178. if (!success) {
  179. ctx.status = 404;
  180. ctx.body = { success: false, error: '素材不存在' };
  181. return;
  182. }
  183. ctx.body = { success: true };
  184. });
  185. // ============ 快捷入口 ============
  186. /**
  187. * POST /api/video/books/:bookId/generate
  188. * 从书籍生成视频项目
  189. */
  190. router.post('/books/:bookId/generate', async (ctx) => {
  191. const bookId = Number(ctx.params.bookId);
  192. const userId = ctx.state.user?.id;
  193. const project = await createVideoProjectFromBook(bookId, userId);
  194. if (!project) {
  195. ctx.status = 404;
  196. ctx.body = { success: false, error: '书籍不存在' };
  197. return;
  198. }
  199. ctx.body = { success: true, data: project };
  200. });
  201. export default router;