Raven10492 commited on
Commit
77e074a
·
verified ·
1 Parent(s): 7dd9c94

Upload 11 files

Browse files
Dockerfile CHANGED
@@ -1,31 +1,31 @@
1
- # Use an official Python runtime as a parent image
2
- FROM python:3.9-slim
3
-
4
- # Create a non-root user
5
- RUN useradd -m -u 1000 user
6
-
7
- # Switch to the new user
8
- USER user
9
-
10
- # Set home to the user's home directory and update PATH
11
- ENV HOME=/home/user \
12
- PATH=/home/user/.local/bin:$PATH
13
-
14
- # Set the working directory
15
- WORKDIR $HOME/app
16
-
17
- # Install dependencies
18
- COPY --chown=user backend/requirements.txt .
19
- RUN pip install --no-cache-dir -r requirements.txt
20
-
21
- # Copy the application code
22
- COPY --chown=user . .
23
-
24
- # Set the working directory to the backend
25
- WORKDIR $HOME/app/backend
26
-
27
- # Expose the port the app runs on
28
- EXPOSE 7860
29
-
30
- # Run the application
31
  CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
 
1
+ # Use an official Python runtime as a parent image
2
+ FROM python:3.9-slim
3
+
4
+ # Create a non-root user
5
+ RUN useradd -m -u 1000 user
6
+
7
+ # Switch to the new user
8
+ USER user
9
+
10
+ # Set home to the user's home directory and update PATH
11
+ ENV HOME=/home/user \
12
+ PATH=/home/user/.local/bin:$PATH
13
+
14
+ # Set the working directory
15
+ WORKDIR $HOME/app
16
+
17
+ # Install dependencies
18
+ COPY --chown=user backend/requirements.txt .
19
+ RUN pip install --no-cache-dir -r requirements.txt
20
+
21
+ # Copy the application code
22
+ COPY --chown=user . .
23
+
24
+ # Set the working directory to the backend
25
+ WORKDIR $HOME/app/backend
26
+
27
+ # Expose the port the app runs on
28
+ EXPOSE 7860
29
+
30
+ # Run the application
31
  CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,20 +1,20 @@
1
- ---
2
- title: VEO3 Video Generator
3
- emoji: 📹
4
- colorFrom: blue
5
- colorTo: green
6
- sdk: docker
7
- app_port: 7860
8
- ---
9
-
10
- # VEO3 Video Generator
11
-
12
- This is a simple web application to generate videos using Google's VEO3 models via a Vertex Express Mode Key loophole.
13
-
14
- ## How to use
15
-
16
- 1. Enter your Vertex Express Key.
17
- 2. Enter a text prompt.
18
- 3. Optionally, upload an image or video.
19
- 4. Select the desired parameters.
20
  5. Click "Generate Video".
 
1
+ ---
2
+ title: VEO3 Video Generator
3
+ emoji: 📹
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: docker
7
+ app_port: 7860
8
+ ---
9
+
10
+ # VEO3 Video Generator
11
+
12
+ This is a simple web application to generate videos using Google's VEO3 models via a Vertex Express Mode Key loophole.
13
+
14
+ ## How to use
15
+
16
+ 1. Enter your Vertex Express Key.
17
+ 2. Enter a text prompt.
18
+ 3. Optionally, upload an image or video.
19
+ 4. Select the desired parameters.
20
  5. Click "Generate Video".
backend/requirements.txt CHANGED
@@ -1,6 +1,7 @@
1
- fastapi
2
- uvicorn[standard]
3
- python-multipart
4
- requests
5
- aiohttp
6
- python-dotenv
 
 
1
+ fastapi
2
+ uvicorn[standard]
3
+ python-multipart
4
+ requests
5
+ aiohttp
6
+ python-dotenv
7
+ Pillow
frontend/app.js CHANGED
@@ -2,6 +2,7 @@ document.addEventListener("DOMContentLoaded", () => {
2
  // --- DOM Element Selection ---
3
  const generateBtn = document.getElementById("generate-btn");
4
  const saveVideoBtn = document.getElementById("save-video-btn");
 
5
  const loader = document.getElementById("loader");
6
  const errorContainer = document.getElementById("error-container");
7
  const placeholder = document.querySelector(".video-container .placeholder");
@@ -51,9 +52,78 @@ document.addEventListener("DOMContentLoaded", () => {
51
  let savedVideos = []; // To track URLs of saved videos
52
  const SAVED_PROMPTS_KEY = 'veoGeneratorSavedPrompts';
53
  const SAVED_KEYS_KEY = 'veoGeneratorSavedKeys';
 
 
54
 
55
- const PRESET_KEYS = [
56
- ];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
  // --- Functions ---
59
 
@@ -344,6 +414,12 @@ document.addEventListener("DOMContentLoaded", () => {
344
  // Update UI state based on loaded settings
345
  updateImageProcessingState();
346
  updateSaveButtonState();
 
 
 
 
 
 
347
  }
348
  updateSaveKeyButtonState(); // Also update key button state on load
349
  }
@@ -658,6 +734,9 @@ document.addEventListener("DOMContentLoaded", () => {
658
  errorContainer.textContent = "所有视频生成请求均失败。";
659
  errorContainer.classList.remove("hidden");
660
  resetCarousel();
 
 
 
661
  }
662
  loader.classList.add("hidden");
663
  });
@@ -688,6 +767,7 @@ document.addEventListener("DOMContentLoaded", () => {
688
  if (videoItems.length > 0) {
689
  currentIndex = 0;
690
  updateCarousel();
 
691
  } else {
692
  resetCarousel(); // No videos were successfully added
693
  }
@@ -821,6 +901,7 @@ document.addEventListener("DOMContentLoaded", () => {
821
 
822
  // --- Initial Load ---
823
  loadSettings();
 
824
  // Set initial state for image processing UI
825
  updateImageProcessingState();
826
  updateSaveButtonState();
 
2
  // --- DOM Element Selection ---
3
  const generateBtn = document.getElementById("generate-btn");
4
  const saveVideoBtn = document.getElementById("save-video-btn");
5
+ const soundToggleBtn = document.getElementById("sound-toggle-btn");
6
  const loader = document.getElementById("loader");
7
  const errorContainer = document.getElementById("error-container");
8
  const placeholder = document.querySelector(".video-container .placeholder");
 
52
  let savedVideos = []; // To track URLs of saved videos
53
  const SAVED_PROMPTS_KEY = 'veoGeneratorSavedPrompts';
54
  const SAVED_KEYS_KEY = 'veoGeneratorSavedKeys';
55
+ const SOUND_SETTINGS_KEY = 'veoGeneratorSoundEnabled';
56
+ let soundEnabled = false; // Default to disabled
57
 
58
+ const PRESET_KEYS = [];
59
+
60
+ // --- Notification Sound ---
61
+ function playNotificationSound() {
62
+ if (!soundEnabled) return; // 如果声音被禁用则直接返回
63
+
64
+ try {
65
+ // 完成音效 - 上升琶音 (C5→E5→G5→C6)
66
+ const audioContext = new (window.AudioContext || window.webkitAudioContext)();
67
+ const notes = [523.25, 659.25, 783.99, 1046.50]; // C5, E5, G5, C6
68
+
69
+ notes.forEach((frequency, index) => {
70
+ setTimeout(() => {
71
+ const oscillator = audioContext.createOscillator();
72
+ const gainNode = audioContext.createGain();
73
+
74
+ oscillator.connect(gainNode);
75
+ gainNode.connect(audioContext.destination);
76
+
77
+ oscillator.frequency.setValueAtTime(frequency, audioContext.currentTime);
78
+ oscillator.type = 'sine';
79
+
80
+ // 最大音量设置
81
+ gainNode.gain.setValueAtTime(0, audioContext.currentTime);
82
+ gainNode.gain.linearRampToValueAtTime(1.0, audioContext.currentTime + 0.01);
83
+ gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3);
84
+
85
+ oscillator.start(audioContext.currentTime);
86
+ oscillator.stop(audioContext.currentTime + 0.3);
87
+ }, index * 100);
88
+ });
89
+ } catch (error) {
90
+ console.log('无法播放提示音:', error);
91
+ }
92
+ }
93
+
94
+ // --- Sound Toggle Logic ---
95
+ function updateSoundButtonState() {
96
+ if (soundEnabled) {
97
+ soundToggleBtn.classList.remove('disabled');
98
+ soundToggleBtn.innerHTML = feather.icons['volume-2'].toSvg();
99
+ soundToggleBtn.title = "提示音已开启,点击关闭";
100
+ } else {
101
+ soundToggleBtn.classList.add('disabled');
102
+ soundToggleBtn.innerHTML = feather.icons['volume-x'].toSvg();
103
+ soundToggleBtn.title = "提示音已关闭,点击开启";
104
+ }
105
+ }
106
+
107
+ function toggleSound() {
108
+ soundEnabled = !soundEnabled;
109
+ localStorage.setItem(SOUND_SETTINGS_KEY, JSON.stringify(soundEnabled));
110
+ updateSoundButtonState();
111
+
112
+ // 如果开启了声音,播放一次提示音作为反馈
113
+ if (soundEnabled) {
114
+ playNotificationSound();
115
+ }
116
+ }
117
+
118
+ function loadSoundSettings() {
119
+ const savedSoundSettings = localStorage.getItem(SOUND_SETTINGS_KEY);
120
+ if (savedSoundSettings !== null) {
121
+ soundEnabled = JSON.parse(savedSoundSettings);
122
+ }
123
+ updateSoundButtonState();
124
+ }
125
+
126
+ soundToggleBtn.addEventListener('click', toggleSound);
127
 
128
  // --- Functions ---
129
 
 
414
  // Update UI state based on loaded settings
415
  updateImageProcessingState();
416
  updateSaveButtonState();
417
+ } else {
418
+ // If no settings, load the default preset key
419
+ if (PRESET_KEYS.length > 0) {
420
+ apiKeyInput.value = PRESET_KEYS[0].value;
421
+ saveSettings(); // Save this default state
422
+ }
423
  }
424
  updateSaveKeyButtonState(); // Also update key button state on load
425
  }
 
734
  errorContainer.textContent = "所有视频生成请求均失败。";
735
  errorContainer.classList.remove("hidden");
736
  resetCarousel();
737
+ } else {
738
+ // At least one video was generated successfully
739
+ playNotificationSound();
740
  }
741
  loader.classList.add("hidden");
742
  });
 
767
  if (videoItems.length > 0) {
768
  currentIndex = 0;
769
  updateCarousel();
770
+ playNotificationSound(); // Play notification sound for successful generation
771
  } else {
772
  resetCarousel(); // No videos were successfully added
773
  }
 
901
 
902
  // --- Initial Load ---
903
  loadSettings();
904
+ loadSoundSettings(); // Load sound settings
905
  // Set initial state for image processing UI
906
  updateImageProcessingState();
907
  updateSaveButtonState();
frontend/index.html CHANGED
@@ -13,6 +13,9 @@
13
  <header class="app-header">
14
  <h1>VEO Video Generator</h1>
15
  <div class="header-actions">
 
 
 
16
  <button id="save-video-btn" class="save-button" disabled>
17
  <i data-feather="save"></i>
18
  <span>保存此视频到服务器</span>
 
13
  <header class="app-header">
14
  <h1>VEO Video Generator</h1>
15
  <div class="header-actions">
16
+ <button id="sound-toggle-btn" class="sound-toggle-button" title="提示音开关">
17
+ <i data-feather="volume-2"></i>
18
+ </button>
19
  <button id="save-video-btn" class="save-button" disabled>
20
  <i data-feather="save"></i>
21
  <span>保存此视频到服务器</span>
frontend/style.css CHANGED
@@ -50,6 +50,7 @@ body {
50
  .header-actions {
51
  display: flex;
52
  align-items: center;
 
53
  }
54
 
55
  .save-button {
@@ -65,7 +66,6 @@ body {
65
  display: flex;
66
  align-items: center;
67
  gap: 8px;
68
- margin-right: 10px; /* Add some space between buttons */
69
  }
70
 
71
  .save-button:not(:disabled) {
@@ -103,6 +103,34 @@ body {
103
  background-color: var(--accent-color-hover);
104
  }
105
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  .main-content {
107
  display: grid;
108
  grid-template-columns: 50fr 50fr;
 
50
  .header-actions {
51
  display: flex;
52
  align-items: center;
53
+ gap: 10px;
54
  }
55
 
56
  .save-button {
 
66
  display: flex;
67
  align-items: center;
68
  gap: 8px;
 
69
  }
70
 
71
  .save-button:not(:disabled) {
 
103
  background-color: var(--accent-color-hover);
104
  }
105
 
106
+ .sound-toggle-button {
107
+ background-color: #ffc107; /* 黄色 */
108
+ color: #212529;
109
+ border: none;
110
+ padding: 10px;
111
+ border-radius: 6px;
112
+ cursor: pointer;
113
+ width: 40px;
114
+ height: 40px;
115
+ transition: background-color 0.2s ease, opacity 0.2s ease;
116
+ display: flex;
117
+ align-items: center;
118
+ justify-content: center;
119
+ }
120
+
121
+ .sound-toggle-button:hover {
122
+ background-color: #e0a800;
123
+ }
124
+
125
+ .sound-toggle-button.disabled {
126
+ background-color: #6c757d;
127
+ opacity: 0.6;
128
+ }
129
+
130
+ .sound-toggle-button.disabled:hover {
131
+ background-color: #5a6268;
132
+ }
133
+
134
  .main-content {
135
  display: grid;
136
  grid-template-columns: 50fr 50fr;
sound-test.html ADDED
@@ -0,0 +1,279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Web Audio API 声音测试</title>
7
+ <style>
8
+ body {
9
+ font-family: Arial, sans-serif;
10
+ max-width: 800px;
11
+ margin: 0 auto;
12
+ padding: 20px;
13
+ background: #f5f5f5;
14
+ }
15
+ .container {
16
+ background: white;
17
+ padding: 30px;
18
+ border-radius: 10px;
19
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
20
+ }
21
+ h1 {
22
+ color: #333;
23
+ text-align: center;
24
+ margin-bottom: 30px;
25
+ }
26
+ .sound-group {
27
+ margin-bottom: 25px;
28
+ padding: 15px;
29
+ border: 1px solid #ddd;
30
+ border-radius: 8px;
31
+ background: #fafafa;
32
+ }
33
+ .sound-group h3 {
34
+ margin-top: 0;
35
+ color: #555;
36
+ }
37
+ button {
38
+ background: #007bff;
39
+ color: white;
40
+ border: none;
41
+ padding: 10px 20px;
42
+ margin: 5px;
43
+ border-radius: 5px;
44
+ cursor: pointer;
45
+ font-size: 14px;
46
+ }
47
+ button:hover {
48
+ background: #0056b3;
49
+ }
50
+ button:active {
51
+ transform: translateY(1px);
52
+ }
53
+ .params {
54
+ margin: 10px 0;
55
+ font-size: 12px;
56
+ color: #666;
57
+ }
58
+ .custom-controls {
59
+ display: grid;
60
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
61
+ gap: 10px;
62
+ margin: 15px 0;
63
+ }
64
+ .control-group {
65
+ display: flex;
66
+ flex-direction: column;
67
+ }
68
+ label {
69
+ font-size: 12px;
70
+ color: #666;
71
+ margin-bottom: 5px;
72
+ }
73
+ input[type="range"] {
74
+ width: 100%;
75
+ }
76
+ input[type="number"] {
77
+ width: 80px;
78
+ padding: 5px;
79
+ border: 1px solid #ccc;
80
+ border-radius: 3px;
81
+ }
82
+ .value-display {
83
+ font-size: 12px;
84
+ color: #333;
85
+ font-weight: bold;
86
+ }
87
+ </style>
88
+ </head>
89
+ <body>
90
+ <div class="container">
91
+ <h1>🔔 Web Audio API 声音测试</h1>
92
+
93
+ <!-- 预设音效 -->
94
+ <div class="sound-group">
95
+ <h3>📢 经典提示音</h3>
96
+ <button onclick="playSound('ding')">系统叮声 (800Hz)</button>
97
+ <button onclick="playSound('beep')">电子哔声 (1000Hz)</button>
98
+ <button onclick="playSound('boop')">温柔提示 (600Hz)</button>
99
+ <button onclick="playSound('chime')">悦耳铃声 (双音)</button>
100
+ </div>
101
+
102
+ <div class="sound-group">
103
+ <h3>🎵 不同波形</h3>
104
+ <button onclick="playWaveform('sine', 800)">正弦波 (Sine)</button>
105
+ <button onclick="playWaveform('square', 800)">方波 (Square)</button>
106
+ <button onclick="playWaveform('triangle', 800)">三角波 (Triangle)</button>
107
+ <button onclick="playWaveform('sawtooth', 800)">锯齿波 (Sawtooth)</button>
108
+ </div>
109
+
110
+ <div class="sound-group">
111
+ <h3>🎶 音符音效</h3>
112
+ <button onclick="playNote(523.25)">C5 (Do)</button>
113
+ <button onclick="playNote(587.33)">D5 (Re)</button>
114
+ <button onclick="playNote(659.25)">E5 (Mi)</button>
115
+ <button onclick="playNote(698.46)">F5 (Fa)</button>
116
+ <button onclick="playNote(783.99)">G5 (Sol)</button>
117
+ </div>
118
+
119
+ <div class="sound-group">
120
+ <h3>🔧 自定义音效</h3>
121
+ <div class="custom-controls">
122
+ <div class="control-group">
123
+ <label>频率 (Hz)</label>
124
+ <input type="range" id="frequency" min="200" max="2000" value="800" oninput="updateValue('frequency')">
125
+ <span class="value-display" id="frequency-value">800</span>
126
+ </div>
127
+ <div class="control-group">
128
+ <label>音量 (0-1)</label>
129
+ <input type="range" id="volume" min="0" max="1" step="0.1" value="0.3" oninput="updateValue('volume')">
130
+ <span class="value-display" id="volume-value">0.3</span>
131
+ </div>
132
+ <div class="control-group">
133
+ <label>持续时间 (秒)</label>
134
+ <input type="range" id="duration" min="0.1" max="2" step="0.1" value="0.5" oninput="updateValue('duration')">
135
+ <span class="value-display" id="duration-value">0.5</span>
136
+ </div>
137
+ <div class="control-group">
138
+ <label>波形类型</label>
139
+ <select id="waveform">
140
+ <option value="sine">正弦波</option>
141
+ <option value="square">方波</option>
142
+ <option value="triangle">三角波</option>
143
+ <option value="sawtooth">锯齿波</option>
144
+ </select>
145
+ </div>
146
+ </div>
147
+ <button onclick="playCustomSound()">🎵 播放自定义音效</button>
148
+ </div>
149
+
150
+ <div class="sound-group">
151
+ <h3>🚨 特殊音效</h3>
152
+ <button onclick="playComplexSound('success')">✅ 成功音效</button>
153
+ <button onclick="playComplexSound('error')">❌ 错误音效</button>
154
+ <button onclick="playComplexSound('notification')">🔔 通知音效</button>
155
+ <button onclick="playComplexSound('complete')">🎉 完成音效</button>
156
+ </div>
157
+ </div>
158
+
159
+ <script>
160
+ // 更新滑块显示值
161
+ function updateValue(id) {
162
+ const slider = document.getElementById(id);
163
+ const display = document.getElementById(id + '-value');
164
+ display.textContent = slider.value;
165
+ }
166
+
167
+ // 基础音效播放函数
168
+ function playTone(frequency, duration = 0.5, volume = 0.3, waveType = 'sine') {
169
+ try {
170
+ const audioContext = new (window.AudioContext || window.webkitAudioContext)();
171
+ const oscillator = audioContext.createOscillator();
172
+ const gainNode = audioContext.createGain();
173
+
174
+ oscillator.connect(gainNode);
175
+ gainNode.connect(audioContext.destination);
176
+
177
+ oscillator.frequency.setValueAtTime(frequency, audioContext.currentTime);
178
+ oscillator.type = waveType;
179
+
180
+ // 音量渐入渐出
181
+ gainNode.gain.setValueAtTime(0, audioContext.currentTime);
182
+ gainNode.gain.linearRampToValueAtTime(volume, audioContext.currentTime + 0.01);
183
+ gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + duration);
184
+
185
+ oscillator.start(audioContext.currentTime);
186
+ oscillator.stop(audioContext.currentTime + duration);
187
+ } catch (error) {
188
+ console.log('播放失败:', error);
189
+ alert('您的浏览器不支持 Web Audio API');
190
+ }
191
+ }
192
+
193
+ // 预设音效
194
+ function playSound(type) {
195
+ switch(type) {
196
+ case 'ding':
197
+ playTone(800, 0.5, 0.3, 'sine');
198
+ break;
199
+ case 'beep':
200
+ playTone(1000, 0.3, 0.4, 'square');
201
+ break;
202
+ case 'boop':
203
+ playTone(600, 0.4, 0.25, 'sine');
204
+ break;
205
+ case 'chime':
206
+ // 双音和弦
207
+ playTone(523.25, 0.8, 0.2, 'sine'); // C5
208
+ setTimeout(() => playTone(659.25, 0.6, 0.15, 'sine'), 100); // E5
209
+ break;
210
+ }
211
+ }
212
+
213
+ // 不同波形测试
214
+ function playWaveform(waveType, frequency) {
215
+ playTone(frequency, 0.6, 0.3, waveType);
216
+ }
217
+
218
+ // 音符播放
219
+ function playNote(frequency) {
220
+ playTone(frequency, 0.8, 0.25, 'sine');
221
+ }
222
+
223
+ // 自定义音效
224
+ function playCustomSound() {
225
+ const frequency = document.getElementById('frequency').value;
226
+ const volume = document.getElementById('volume').value;
227
+ const duration = document.getElementById('duration').value;
228
+ const waveform = document.getElementById('waveform').value;
229
+
230
+ playTone(parseFloat(frequency), parseFloat(duration), parseFloat(volume), waveform);
231
+ }
232
+
233
+ // 复杂音效
234
+ function playComplexSound(type) {
235
+ try {
236
+ const audioContext = new (window.AudioContext || window.webkitAudioContext)();
237
+
238
+ switch(type) {
239
+ case 'success':
240
+ // 上升三和弦
241
+ playTone(523.25, 0.2, 0.2, 'sine'); // C5
242
+ setTimeout(() => playTone(659.25, 0.2, 0.2, 'sine'), 150); // E5
243
+ setTimeout(() => playTone(783.99, 0.4, 0.25, 'sine'), 300); // G5
244
+ break;
245
+
246
+ case 'error':
247
+ // 下降的不和谐音
248
+ playTone(400, 0.3, 0.3, 'square');
249
+ setTimeout(() => playTone(300, 0.4, 0.25, 'square'), 200);
250
+ break;
251
+
252
+ case 'notification':
253
+ // 快速双击
254
+ playTone(800, 0.15, 0.3, 'sine');
255
+ setTimeout(() => playTone(800, 0.15, 0.3, 'sine'), 200);
256
+ break;
257
+
258
+ case 'complete':
259
+ // 完成音效 - 上升琶音
260
+ const notes = [523.25, 659.25, 783.99, 1046.50]; // C5, E5, G5, C6
261
+ notes.forEach((note, index) => {
262
+ setTimeout(() => playTone(note, 0.3, 0.2, 'sine'), index * 100);
263
+ });
264
+ break;
265
+ }
266
+ } catch (error) {
267
+ console.log('播放复杂音效失败:', error);
268
+ }
269
+ }
270
+
271
+ // 初始化显示值
272
+ document.addEventListener('DOMContentLoaded', () => {
273
+ updateValue('frequency');
274
+ updateValue('volume');
275
+ updateValue('duration');
276
+ });
277
+ </script>
278
+ </body>
279
+ </html>