duqing2026 commited on
Commit
c4aaa51
·
1 Parent(s): ebb2801

Add MP4 conversion feature

Browse files
Files changed (2) hide show
  1. app.py +36 -3
  2. templates/index.html +43 -10
app.py CHANGED
@@ -14,9 +14,42 @@ os.makedirs(app.config['OUTPUT_FOLDER'], exist_ok=True)
14
  def index():
15
  return render_template('index.html')
16
 
17
- @app.route('/health')
18
- def health():
19
- return jsonify({"status": "healthy"})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
  if __name__ == '__main__':
22
  app.run(host='0.0.0.0', port=7860)
 
14
  def index():
15
  return render_template('index.html')
16
 
17
+ @app.route('/convert', methods=['POST'])
18
+ def convert():
19
+ if 'video' not in request.files:
20
+ return jsonify({"error": "No video file"}), 400
21
+
22
+ video = request.files['video']
23
+ filename = str(uuid.uuid4())
24
+ input_path = os.path.join(app.config['UPLOAD_FOLDER'], filename + '.webm')
25
+ output_path = os.path.join(app.config['OUTPUT_FOLDER'], filename + '.mp4')
26
+
27
+ video.save(input_path)
28
+
29
+ # FFmpeg conversion
30
+ # -vf "pad=ceil(iw/2)*2:ceil(ih/2)*2" ensures dimensions are even for libx264
31
+ cmd = [
32
+ 'ffmpeg', '-y', '-i', input_path,
33
+ '-c:v', 'libx264', '-preset', 'fast', '-crf', '23',
34
+ '-c:a', 'aac', '-b:a', '192k',
35
+ '-vf', 'pad=ceil(iw/2)*2:ceil(ih/2)*2',
36
+ output_path
37
+ ]
38
+
39
+ try:
40
+ subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
41
+ return jsonify({"url": f"/download/{filename}.mp4"})
42
+ except subprocess.CalledProcessError as e:
43
+ return jsonify({"error": "Conversion failed", "details": str(e)}), 500
44
+ finally:
45
+ # Cleanup input
46
+ if os.path.exists(input_path):
47
+ os.remove(input_path)
48
+
49
+ @app.route('/download/<filename>')
50
+ def download_file(filename):
51
+ return send_file(os.path.join(app.config['OUTPUT_FOLDER'], filename), as_attachment=True)
52
+
53
 
54
  if __name__ == '__main__':
55
  app.run(host='0.0.0.0', port=7860)
templates/index.html CHANGED
@@ -159,10 +159,10 @@
159
  <span>{{ isPlaying ? '暂停预览' : '播放预览' }}</span>
160
  </button>
161
 
162
- <button @click="startRecording" :disabled="!audioFile || isRecording"
163
  class="w-full py-3 rounded-lg font-semibold flex items-center justify-center space-x-2 transition-all disabled:opacity-50 disabled:cursor-not-allowed bg-indigo-600 hover:bg-indigo-700 text-white">
164
- <i class="fa-solid" :class="isRecording ? 'fa-spinner fa-spin' : 'fa-video'"></i>
165
- <span>{{ isRecording ? '正在录制...' : '开始生成视频' }}</span>
166
  </button>
167
 
168
  <div v-if="isRecording" class="text-center text-sm text-gray-500 mt-2">
@@ -200,12 +200,13 @@
200
  </div>
201
 
202
  <div class="bg-gray-100 rounded-lg p-4 mb-6 text-center">
203
- <p class="text-gray-600 mb-2">您的视频已生成 (WebM格式)</p>
204
- <p class="text-xs text-gray-500">注意:WebM 格式在 Windows/Android 上兼容性良好。如需 MP4,请使用在线工具转换。</p>
 
205
  </div>
206
 
207
  <div class="flex space-x-3">
208
- <a :href="videoUrl" download="audiogram.webm" class="flex-1 bg-indigo-600 text-white py-3 rounded-lg font-semibold text-center hover:bg-indigo-700 transition-colors">
209
  <i class="fa-solid fa-download mr-2"></i> 下载视频
210
  </a>
211
  </div>
@@ -224,8 +225,10 @@
224
  const bgImageSrc = ref(null);
225
  const isPlaying = ref(false);
226
  const isRecording = ref(false);
 
227
  const showResultModal = ref(false);
228
  const videoUrl = ref('');
 
229
 
230
  // Settings
231
  const aspectRatio = ref(1); // 1:1 default
@@ -540,11 +543,39 @@
540
  }
541
  };
542
 
543
- mediaRecorder.onstop = () => {
544
  const blob = new Blob(recordedChunks, { type: 'video/webm' });
545
- videoUrl.value = URL.createObjectURL(blob);
546
- showResultModal.value = true;
547
- isRecording.value = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
548
  };
549
 
550
  mediaRecorder.start();
@@ -580,6 +611,8 @@
580
  duration,
581
  showResultModal,
582
  videoUrl,
 
 
583
  handleAudioUpload,
584
  handleImageUpload,
585
  setAspectRatio,
 
159
  <span>{{ isPlaying ? '暂停预览' : '播放预览' }}</span>
160
  </button>
161
 
162
+ <button @click="startRecording" :disabled="!audioFile || isRecording || isConverting"
163
  class="w-full py-3 rounded-lg font-semibold flex items-center justify-center space-x-2 transition-all disabled:opacity-50 disabled:cursor-not-allowed bg-indigo-600 hover:bg-indigo-700 text-white">
164
+ <i class="fa-solid" :class="isRecording ? 'fa-spinner fa-spin' : (isConverting ? 'fa-cog fa-spin' : 'fa-video')"></i>
165
+ <span>{{ isRecording ? '正在录制...' : (isConverting ? '正在转码为 MP4...' : '开始生成视频') }}</span>
166
  </button>
167
 
168
  <div v-if="isRecording" class="text-center text-sm text-gray-500 mt-2">
 
200
  </div>
201
 
202
  <div class="bg-gray-100 rounded-lg p-4 mb-6 text-center">
203
+ <p class="text-gray-600 mb-2">您的视频已生成 ({{ videoType.toUpperCase() }}格式)</p>
204
+ <p class="text-xs text-gray-500" v-if="videoType === 'webm'">注意:WebM 格式在 Windows/Android 上兼容性良好。如需 MP4,请使用在线工具转换。</p>
205
+ <p class="text-xs text-gray-500" v-else>MP4 格式,兼容所有平台 (iOS/Android/PC)。</p>
206
  </div>
207
 
208
  <div class="flex space-x-3">
209
+ <a :href="videoUrl" :download="'audiogram.' + videoType" class="flex-1 bg-indigo-600 text-white py-3 rounded-lg font-semibold text-center hover:bg-indigo-700 transition-colors">
210
  <i class="fa-solid fa-download mr-2"></i> 下载视频
211
  </a>
212
  </div>
 
225
  const bgImageSrc = ref(null);
226
  const isPlaying = ref(false);
227
  const isRecording = ref(false);
228
+ const isConverting = ref(false);
229
  const showResultModal = ref(false);
230
  const videoUrl = ref('');
231
+ const videoType = ref('webm');
232
 
233
  // Settings
234
  const aspectRatio = ref(1); // 1:1 default
 
543
  }
544
  };
545
 
546
+ mediaRecorder.onstop = async () => {
547
  const blob = new Blob(recordedChunks, { type: 'video/webm' });
548
+ // Upload for conversion
549
+ isConverting.value = true;
550
+
551
+ const formData = new FormData();
552
+ formData.append('video', blob, 'recording.webm');
553
+
554
+ try {
555
+ const response = await fetch('/convert', {
556
+ method: 'POST',
557
+ body: formData
558
+ });
559
+
560
+ if (response.ok) {
561
+ const data = await response.json();
562
+ videoUrl.value = data.url;
563
+ videoType.value = 'mp4';
564
+ } else {
565
+ // Fallback to webm if backend fails
566
+ console.error("Conversion failed, falling back to WebM");
567
+ videoUrl.value = URL.createObjectURL(blob);
568
+ videoType.value = 'webm';
569
+ }
570
+ } catch (e) {
571
+ console.error("Network error, falling back to WebM", e);
572
+ videoUrl.value = URL.createObjectURL(blob);
573
+ videoType.value = 'webm';
574
+ } finally {
575
+ isConverting.value = false;
576
+ showResultModal.value = true;
577
+ isRecording.value = false;
578
+ }
579
  };
580
 
581
  mediaRecorder.start();
 
611
  duration,
612
  showResultModal,
613
  videoUrl,
614
+ isConverting,
615
+ videoType,
616
  handleAudioUpload,
617
  handleImageUpload,
618
  setAspectRatio,