Browse Source

优化生成对口型逻辑

黎海 2 months ago
parent
commit
cc605075ea
2 changed files with 201 additions and 111 deletions
  1. 39 40
      src/pages/projectDetail/projectDetail.js
  2. 162 71
      src/services/lipSyncService.js

+ 39 - 40
src/pages/projectDetail/projectDetail.js

@@ -102,7 +102,7 @@ const ProjectDetail = () => {
102 102
         setProjectAudioPath(null);
103 103
         setShowAudioPlayer(false);
104 104
       }
105
-      
105
+
106 106
       // 加载项目视频路径
107 107
       if (projectData && projectData.video_path) {
108 108
         setProjectVideoPath(projectData.video_path);
@@ -1869,9 +1869,9 @@ const ProjectDetail = () => {
1869 1869
       // 将本地文件上传到Coze获取可访问链接
1870 1870
       toast.info('正在将音频上传到云端...');
1871 1871
       const audioUrl = await cozeUploadService.uploadFileAndGetLink(projectAudioFile);
1872
-      
1872
+
1873 1873
       // 更新项目数据库记录 - 同时保存本地路径和云端链接
1874
-      await bookService.updateBook(parseInt(projectId), { 
1874
+      await bookService.updateBook(parseInt(projectId), {
1875 1875
         audio_path: result.filePath,
1876 1876
         audio_url: audioUrl // 新增字段保存云端链接
1877 1877
       });
@@ -1906,31 +1906,31 @@ const ProjectDetail = () => {
1906 1906
         toast.error('请先上传音频和视频文件');
1907 1907
         return;
1908 1908
       }
1909
-      
1909
+
1910 1910
       // 确认是否开始处理
1911 1911
       if (!window.confirm('确定要开始生成对口型视频吗?此过程可能需要几分钟时间。')) {
1912 1912
         return;
1913 1913
       }
1914
-      
1914
+
1915 1915
       setLipSyncProcessing(true);
1916 1916
       setLipSyncProgress(0);
1917
-      
1917
+
1918 1918
       let audioUrl = project.audio_url; // 尝试使用已保存的云端链接
1919 1919
       let videoUrl = project.video_url; // 尝试使用已保存的云端链接
1920
-      
1920
+
1921 1921
       // 如果没有保存的云端链接,则进行上传
1922 1922
       if (!audioUrl || !videoUrl) {
1923 1923
         toast.info('正在上传音频和视频文件...');
1924
-        
1924
+
1925 1925
         // 异步上传音频和视频
1926 1926
         const uploadResults = await Promise.all([
1927 1927
           !audioUrl ? cozeUploadService.uploadProjectMedia(project.audio_path) : Promise.resolve(audioUrl),
1928 1928
           !videoUrl ? cozeUploadService.uploadProjectMedia(project.video_path) : Promise.resolve(videoUrl)
1929 1929
         ]);
1930
-        
1930
+
1931 1931
         audioUrl = uploadResults[0];
1932 1932
         videoUrl = uploadResults[1];
1933
-        
1933
+
1934 1934
         // 如果成功获取到链接,保存到数据库中以便下次使用
1935 1935
         if (audioUrl && audioUrl !== project.audio_url) {
1936 1936
           try {
@@ -1940,7 +1940,7 @@ const ProjectDetail = () => {
1940 1940
             console.error('保存音频云端链接失败:', error);
1941 1941
           }
1942 1942
         }
1943
-        
1943
+
1944 1944
         if (videoUrl && videoUrl !== project.video_url) {
1945 1945
           try {
1946 1946
             await bookService.updateBook(project.id, { video_url: videoUrl });
@@ -1952,13 +1952,13 @@ const ProjectDetail = () => {
1952 1952
       } else {
1953 1953
         toast.info('使用已保存的音频和视频链接...');
1954 1954
       }
1955
-      
1955
+
1956 1956
       if (!audioUrl || !videoUrl) {
1957 1957
         throw new Error('上传音频或视频文件失败');
1958 1958
       }
1959
-      
1959
+
1960 1960
       toast.success('文件准备完成,开始处理对口型...');
1961
-      
1961
+
1962 1962
       // 开始处理对口型任务
1963 1963
       lipSyncService.processLipSyncTask(
1964 1964
         videoUrl,
@@ -1973,15 +1973,15 @@ const ProjectDetail = () => {
1973 1973
           console.log('对口型处理完成:', resultUrl);
1974 1974
           setLipSyncResultUrl(resultUrl);
1975 1975
           setLipSyncProcessing(false);
1976
-          
1976
+
1977 1977
           // 保存结果到项目
1978 1978
           try {
1979 1979
             await bookService.updateBook(project.id, {
1980 1980
               lip_sync_video_path: resultUrl,
1981
-              video_path: resultUrl, // 同时更新视频路径
1981
+              // video_path: resultUrl, // 同时更新视频路径
1982 1982
               lip_sync_task_id: taskInfo.task_id
1983 1983
             });
1984
-            
1984
+
1985 1985
             toast.success('对口型视频已保存到项目');
1986 1986
             // 重新加载项目详情
1987 1987
             await loadProjectDetail(true);
@@ -2044,7 +2044,7 @@ const ProjectDetail = () => {
2044 2044
               <Card.Body>
2045 2045
                 <Row className="align-items-center">
2046 2046
                   {/* 项目音频 */}
2047
-                  <Col md={3}>
2047
+                  <Col md={6}>
2048 2048
                     <Form.Group>
2049 2049
                       <Form.Label>项目音频</Form.Label>
2050 2050
                       {projectAudioPath && projectAudioPath.trim() !== '' ? (
@@ -2063,16 +2063,16 @@ const ProjectDetail = () => {
2063 2063
                       )}
2064 2064
                     </Form.Group>
2065 2065
                   </Col>
2066
-                  
2066
+
2067 2067
                   {/* 项目视频 */}
2068 2068
                   <Col md={3}>
2069 2069
                     <Form.Group>
2070 2070
                       <Form.Label>项目视频</Form.Label>
2071 2071
                       {project.video_path && project.video_path.trim() !== '' ? (
2072 2072
                         <div className="project-video-container">
2073
-                          <video 
2074
-                            className="w-100" 
2075
-                            controls 
2073
+                          <video
2074
+                            className="w-100"
2075
+                            controls
2076 2076
                             src={project.video_path}
2077 2077
                             style={{ maxHeight: '150px' }}
2078 2078
                           >
@@ -2086,17 +2086,17 @@ const ProjectDetail = () => {
2086 2086
                       )}
2087 2087
                     </Form.Group>
2088 2088
                   </Col>
2089
-                  
2089
+
2090 2090
                   {/* 对口型生成区域 */}
2091 2091
                   <Col md={3}>
2092 2092
                     <Form.Group>
2093 2093
                       <Form.Label>对口型视频</Form.Label>
2094 2094
                       {lipSyncProcessing ? (
2095 2095
                         <div className="lip-sync-progress">
2096
-                          <ProgressBar 
2097
-                            animated 
2098
-                            now={lipSyncProgress} 
2099
-                            label={`${Math.round(lipSyncProgress)}%`} 
2096
+                          <ProgressBar
2097
+                            animated
2098
+                            now={lipSyncProgress}
2099
+                            label={`${Math.round(lipSyncProgress)}%`}
2100 2100
                             className="mb-2"
2101 2101
                           />
2102 2102
                           <div className="text-center">
@@ -2105,9 +2105,9 @@ const ProjectDetail = () => {
2105 2105
                         </div>
2106 2106
                       ) : project.lip_sync_video_path ? (
2107 2107
                         <div className="lip-sync-video-container">
2108
-                          <video 
2109
-                            className="w-100" 
2110
-                            controls 
2108
+                          <video
2109
+                            className="w-100"
2110
+                            controls
2111 2111
                             src={project.lip_sync_video_path}
2112 2112
                             style={{ maxHeight: '150px' }}
2113 2113
                           >
@@ -2118,12 +2118,12 @@ const ProjectDetail = () => {
2118 2118
                         <Alert variant="info" className="mb-0">
2119 2119
                           <small>
2120 2120
                             暂无对口型视频
2121
-                            {(project.audio_path && project.video_path) ? 
2121
+                            {(project.audio_path && project.video_path) ?
2122 2122
                               <div className="mt-1">
2123 2123
                                 <small>
2124 2124
                                   已上传音频和视频文件,可以点击"生成对口型"按钮开始处理
2125 2125
                                 </small>
2126
-                              </div> : 
2126
+                              </div> :
2127 2127
                               <div className="mt-1">
2128 2128
                                 <small>
2129 2129
                                   需要先上传音频和视频文件才能生成对口型
@@ -2134,10 +2134,6 @@ const ProjectDetail = () => {
2134 2134
                         </Alert>
2135 2135
                       )}
2136 2136
                     </Form.Group>
2137
-                  </Col>
2138
-                  
2139
-                  {/* 对口型操作按钮 */}
2140
-                  <Col md={3} className="d-flex align-items-end">
2141 2137
                     <div className="w-100">
2142 2138
                       <Button
2143 2139
                         variant="primary"
@@ -2161,7 +2157,10 @@ const ProjectDetail = () => {
2161 2157
                       </Button>
2162 2158
                     </div>
2163 2159
                   </Col>
2164
-                  
2160
+
2161
+                  {/* 对口型操作按钮 */}
2162
+
2163
+
2165 2164
                   {/* 项目画风选择 */}
2166 2165
                   <Col md={4}>
2167 2166
                     <Form.Group>
@@ -2391,7 +2390,7 @@ const ProjectDetail = () => {
2391 2390
                                 </div>
2392 2391
                               )}
2393 2392
                             </td>
2394
-                            
2393
+
2395 2394
                             <td>
2396 2395
                               {segment.image_path ? (
2397 2396
                                 <div className="media-cell">
@@ -2469,7 +2468,7 @@ const ProjectDetail = () => {
2469 2468
                                 </div>
2470 2469
                               )}
2471 2470
                             </td>
2472
-                            
2471
+
2473 2472
                             <td>
2474 2473
                               <Dropdown>
2475 2474
                                 <Dropdown.Toggle variant="outline-secondary" size="sm" id={`dropdown-${segment.id}`}>
@@ -2523,7 +2522,7 @@ const ProjectDetail = () => {
2523 2522
           </div>
2524 2523
         )}
2525 2524
       </Container>
2526
-      
2525
+
2527 2526
       {/* 合并分镜对话框 */}
2528 2527
       <Modal show={showMergeModal} onHide={() => setShowMergeModal(false)}>
2529 2528
         <Modal.Header closeButton>

+ 162 - 71
src/services/lipSyncService.js

@@ -4,7 +4,10 @@ import { getLipSyncServer } from '../utils/lipSyncConfig';
4 4
 
5 5
 // 从配置中获取服务器地址
6 6
 const getServerUrl = () => {
7
-  return getLipSyncServer();
7
+  const serverConfig = getLipSyncServer();
8
+  const serverUrl = serverConfig.apiUrl || serverConfig;
9
+  // 移除末尾的斜杠,避免拼接时出现双斜杠
10
+  return serverUrl.replace(/\/$/, '');
8 11
 };
9 12
 
10 13
 /**
@@ -39,74 +42,152 @@ const testServer = async (serverUrl) => {
39 42
 };
40 43
 
41 44
 /**
42
- * 提交对口型合成任务
43
- * @param {string} videoUrl - 视频URL
44
- * @param {string} audioUrl - 音频URL
45
- * @returns {Promise<string>} - 返回任务ID
45
+ * 提交对口型任务
46
+ * @param {string} videoUrl 视频URL
47
+ * @param {string} audioUrl 音频URL
48
+ * @returns {Promise<{id: string}>} 任务信息
46 49
  */
47 50
 const submitLipSyncTask = async (videoUrl, audioUrl) => {
48 51
   try {
49
-    if (!videoUrl || !audioUrl) {
50
-      throw new Error('视频URL和音频URL不能为空');
51
-    }
52
+    console.log('开始提交对口型任务...');
53
+    console.log('视频URL:', videoUrl);
54
+    console.log('音频URL:', audioUrl);
52 55
 
56
+    // 获取服务器地址
53 57
     const serverUrl = getServerUrl();
54
-    const response = await axios.post(`${serverUrl}/direct-lip-sync/`, {
58
+    if (!serverUrl) {
59
+      throw new Error('未配置对口型服务器地址');
60
+    }
61
+
62
+    // 构建请求数据
63
+    const requestData = {
55 64
       video_url: videoUrl,
56 65
       audio_url: audioUrl
57
-    }, {
66
+    };
67
+
68
+    const apiUrl = `${serverUrl}/direct-lip-sync/`;
69
+    console.log('发送对口型任务请求:', {
70
+      url: apiUrl,
71
+      data: requestData
72
+    });
73
+
74
+    // 发送POST请求获取任务ID
75
+    const response = await axios.post(apiUrl, requestData, {
58 76
       headers: {
59 77
         'accept': 'application/json, text/plain, */*',
78
+        'accept-language': 'zh-CN,zh;q=0.9',
60 79
         'content-type': 'application/json',
61 80
         'origin': 'https://heygem.fyshark.com',
62
-        'referer': 'https://heygem.fyshark.com/'
81
+        'priority': 'u=1, i',
82
+        'referer': 'https://heygem.fyshark.com/',
83
+        'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="126"',
84
+        'sec-ch-ua-mobile': '?0',
85
+        'sec-ch-ua-platform': '"macOS"',
86
+        'sec-fetch-dest': 'empty',
87
+        'sec-fetch-mode': 'cors',
88
+        'sec-fetch-site': 'cross-site',
89
+        'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) jianyin_agent/2.4.3 Chrome/126.0.6478.234 Electron/31.7.6 Safari/537.36'
63 90
       }
64 91
     });
65 92
 
66
-    if (response.data && response.data.length > 0 && response.data[0].task_id) {
67
-      console.log('对口型任务提交成功:', response.data[0]);
68
-      return response.data[0].task_id;
69
-    } else {
93
+    console.log('对口型任务提交响应:', response.data);
94
+
95
+    // 检查响应数据
96
+    if (!response.data || !response.data.id) {
70 97
       console.error('对口型任务提交失败:', response.data);
71 98
       throw new Error('提交任务失败,未返回任务ID');
72 99
     }
100
+
101
+    return {
102
+      id: response.data.id
103
+    };
73 104
   } catch (error) {
74 105
     console.error('提交对口型任务错误:', error);
75
-    toast.error(`提交对口型任务失败: ${error.message}`);
106
+    
107
+    // 处理服务器忙碌的情况
108
+    if (error.response && error.response.status === 500 && error.response.data && error.response.data.detail === '503: 服务器忙碌中,请稍后再试') {
109
+      toast.error('服务器正在处理其他任务,请等待当前任务完成后再试');
110
+      throw new Error('服务器正在处理其他任务,请等待当前任务完成后再试');
111
+    }
112
+    
76 113
     throw error;
77 114
   }
78 115
 };
79 116
 
80 117
 /**
81 118
  * 获取对口型任务信息
82
- * @param {string} taskId - 任务ID
83
- * @returns {Promise<Object>} - 返回任务信息
119
+ * @param {string} taskId 任务ID
120
+ * @returns {Promise<{status: string, progress: number, result_url?: string}>} 任务信息
84 121
  */
85 122
 const getLipSyncTaskInfo = async (taskId) => {
86 123
   try {
87
-    if (!taskId) {
88
-      throw new Error('任务ID不能为空');
89
-    }
124
+    console.log('获取对口型任务信息:', taskId);
90 125
 
126
+    // 获取服务器地址
91 127
     const serverUrl = getServerUrl();
92
-    const response = await axios.get(`${serverUrl}/direct-lip-sync/?task_id=${taskId}`, {
128
+    if (!serverUrl) {
129
+      throw new Error('未配置对口型服务器地址');
130
+    }
131
+
132
+    const apiUrl = `${serverUrl}/direct-lip-sync/${taskId}`;
133
+    console.log('获取对口型任务状态:', apiUrl);
134
+
135
+    // 发送GET请求获取任务状态
136
+    const response = await axios.get(apiUrl, {
93 137
       headers: {
94 138
         'accept': 'application/json, text/plain, */*',
139
+        'content-type': 'application/json',
95 140
         'origin': 'https://heygem.fyshark.com',
96
-        'referer': 'https://heygem.fyshark.com/'
141
+        'referer': 'https://heygem.fyshark.com/',
142
+        'sec-ch-ua': '"Chromium";v="134", "Not:A-Brand";v="24", "Google Chrome";v="134"',
143
+        'sec-ch-ua-mobile': '?0',
144
+        'sec-ch-ua-platform': '"macOS"',
145
+        'sec-fetch-dest': 'empty',
146
+        'sec-fetch-mode': 'cors',
147
+        'sec-fetch-site': 'cross-site',
148
+        'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36'
97 149
       }
98 150
     });
99 151
 
100
-    if (response.data && response.data.length > 0) {
101
-      console.log('对口型任务信息:', response.data[0]);
102
-      return response.data[0];
103
-    } else {
104
-      console.error('获取任务信息失败:', response.data);
105
-      throw new Error('获取任务信息失败,未返回有效数据');
152
+    console.log('对口型任务状态响应:', response.data);
153
+
154
+    // 检查响应数据
155
+    if (!response.data) {
156
+      throw new Error('获取任务状态失败,响应数据为空');
157
+    }
158
+
159
+    // 检查任务ID是否匹配
160
+    if (response.data.id !== parseInt(taskId)) {
161
+      throw new Error('未找到指定任务ID的信息');
106 162
     }
163
+
164
+    // 转换状态码为状态文本
165
+    let statusText = 'processing';
166
+    if (response.data.status === 1) {
167
+      statusText = 'processing';
168
+    } else if (response.data.status === 2) {
169
+      statusText = 'completed';
170
+    } else if (response.data.status === 3) {
171
+      statusText = 'failed';
172
+    }
173
+
174
+    // 如果有结果URL,拼接完整的下载地址
175
+    let resultUrl = response.data.result_url;
176
+    if (resultUrl) {
177
+      // 确保路径以/开头
178
+      const formattedPath = resultUrl.startsWith('/') ? resultUrl : `/${resultUrl}`;
179
+      // 修改下载链接的端口为8383
180
+      const downloadUrl = serverUrl.replace(/-80\./, '-8383.');
181
+      resultUrl = `${downloadUrl}/download${formattedPath}`;
182
+    }
183
+
184
+    return {
185
+      status: statusText,
186
+      progress: response.data.progress || 0,
187
+      result_url: resultUrl
188
+    };
107 189
   } catch (error) {
108
-    console.error('获取对口型任务信息错误:', error);
109
-    toast.error(`获取任务状态失败: ${error.message}`);
190
+    console.error('获取对口型任务信息失败:', error);
110 191
     throw error;
111 192
   }
112 193
 };
@@ -127,59 +208,69 @@ const getLipSyncResultUrl = (resultPath) => {
127 208
 };
128 209
 
129 210
 /**
130
- * 提交对口型任务并定期检查进度直到完成
131
- * @param {string} videoUrl - 视频URL
132
- * @param {string} audioUrl - 音频URL
133
- * @param {function} onProgressUpdate - 进度更新回调函数 (progress, status) => void
134
- * @param {function} onComplete - 完成回调函数 (resultUrl) => void
135
- * @param {function} onError - 错误回调函数 (error) => void
136
- * @returns {Promise<void>}
211
+ * 处理对口型任务
212
+ * @param {string} videoUrl 视频URL
213
+ * @param {string} audioUrl 音频URL
214
+ * @param {Function} onProgress 进度回调函数
215
+ * @param {Function} onComplete 完成回调函数
216
+ * @param {Function} onError 错误回调函数
137 217
  */
138
-const processLipSyncTask = async (videoUrl, audioUrl, onProgressUpdate, onComplete, onError) => {
218
+const processLipSyncTask = async (videoUrl, audioUrl, onProgress, onComplete, onError) => {
139 219
   try {
140
-    // 提交任务
141
-    const taskId = await submitLipSyncTask(videoUrl, audioUrl);
142
-    
143
-    // 定义检查间隔(秒)
144
-    const checkInterval = 5;
145
-    let completed = false;
146
-    
147
-    // 轮询检查任务状态
148
-    const checkStatus = async () => {
220
+    console.log('开始处理对口型任务...');
221
+    console.log('视频URL:', videoUrl);
222
+    console.log('音频URL:', audioUrl);
223
+
224
+    // 1. 提交任务
225
+    const taskInfo = await submitLipSyncTask(videoUrl, audioUrl);
226
+    console.log('对口型任务提交成功:', taskInfo);
227
+
228
+    if (!taskInfo || !taskInfo.id) {
229
+      throw new Error('提交任务失败,未返回任务ID');
230
+    }
231
+
232
+    // 2. 轮询任务状态
233
+    const pollInterval = 5000; // 5秒
234
+    const maxAttempts = 120; // 最多等待10分钟
235
+    let attempts = 0;
236
+
237
+    const pollTaskStatus = async () => {
149 238
       try {
150
-        if (completed) return;
151
-        
152
-        const taskInfo = await getLipSyncTaskInfo(taskId);
153
-        
239
+        const statusInfo = await getLipSyncTaskInfo(taskInfo.id);
240
+        console.log('对口型任务状态:', statusInfo);
241
+
154 242
         // 更新进度
155
-        if (onProgressUpdate) {
156
-          onProgressUpdate(taskInfo.progress, taskInfo.status);
243
+        if (onProgress) {
244
+          onProgress(statusInfo.progress, statusInfo.status);
157 245
         }
158
-        
246
+
159 247
         // 检查任务状态
160
-        if (taskInfo.status === 2) { // 已完成
161
-          completed = true;
162
-          const resultUrl = getLipSyncResultUrl(taskInfo.result_url);
248
+        if (statusInfo.status === 'completed') {
163 249
           if (onComplete) {
164
-            onComplete(resultUrl, taskInfo);
250
+            onComplete(statusInfo.result_url, taskInfo);
165 251
           }
166
-        } else if (taskInfo.status === 1) { // 进行中
167
-          // 继续轮询
168
-          setTimeout(checkStatus, checkInterval * 1000);
169
-        } else { // 错误状态
170
-          completed = true;
171
-          throw new Error(`任务状态异常: ${taskInfo.status}`);
252
+          return true;
253
+        } else if (statusInfo.status === 'failed') {
254
+          throw new Error('对口型任务处理失败');
172 255
         }
173
-      } catch (error) {
174
-        if (onError) {
175
-          onError(error);
256
+
257
+        // 继续轮询
258
+        attempts++;
259
+        if (attempts >= maxAttempts) {
260
+          throw new Error('对口型任务超时');
176 261
         }
262
+
263
+        // 等待一段时间后继续轮询
264
+        await new Promise(resolve => setTimeout(resolve, pollInterval));
265
+        return await pollTaskStatus();
266
+      } catch (error) {
267
+        console.error('轮询对口型任务状态失败:', error);
268
+        throw error;
177 269
       }
178 270
     };
179
-    
271
+
180 272
     // 开始轮询
181
-    checkStatus();
182
-    
273
+    await pollTaskStatus();
183 274
   } catch (error) {
184 275
     console.error('处理对口型任务出错:', error);
185 276
     if (onError) {