Add customizable recording beep sound feature

Backend:
- Add beep_sound and beep_enabled configuration options
- Generate default 1000Hz beep (0.5s) on first run
- Add get/set methods for beep sound and enabled state
- Play beep after greeting, before recording starts
- Add /set_beep_sound and /api/beep_enabled endpoints

Frontend:
- Add "Recording Beep" toggle checkbox in settings
- Add "📣 Set Beep" button for each greeting sound
- Show beep status indicator on sounds
- Real-time enable/disable with visual feedback
- Template v1.4.0

Configuration:
- beep_sound: WAV filename for beep (default: "beep.wav")
- beep_enabled: true/false to enable/disable beep

This gives guests a clear audio cue that recording has started.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-27 12:43:30 +07:00
parent b0f1514457
commit 4f6398858e
2 changed files with 163 additions and 4 deletions

View File

@@ -35,6 +35,8 @@
"system": {
"active_greeting": "dialtone.wav",
"extra_button_sound": "button_sound.wav",
"beep_sound": "beep.wav",
"beep_enabled": true,
"greeting_delay_seconds": 0,
"volume": 70
}

View File

@@ -81,7 +81,7 @@ BACKUP_ON_WRITE = BACKUP_CONFIG.get('backup_on_write', True)
WEB_PORT = SYS_CONFIG['web']['port']
# Template version - increment this when HTML template changes
TEMPLATE_VERSION = "1.3.0" # Updated: Added download all recordings feature
TEMPLATE_VERSION = "1.4.0" # Updated: Added recording beep sound feature
# Flask app
app = Flask(__name__)
@@ -250,11 +250,18 @@ class RotaryPhone:
if not os.path.exists(DIALTONE_FILE):
self.generate_default_dialtone()
# Generate default beep if none exists
beep_file = os.path.join(SOUNDS_DIR, "beep.wav")
if not os.path.exists(beep_file):
self.generate_default_beep()
def load_config(self):
"""Load user runtime configuration from JSON file"""
default_config = {
"active_greeting": SYS_CONFIG['system']['active_greeting'],
"extra_button_sound": SYS_CONFIG['system'].get('extra_button_sound', 'button_sound.wav'),
"beep_sound": SYS_CONFIG['system'].get('beep_sound', 'beep.wav'),
"beep_enabled": SYS_CONFIG['system'].get('beep_enabled', True),
"greetings": [],
"volume": SYS_CONFIG['system']['volume'],
"greeting_delay": SYS_CONFIG['system'].get('greeting_delay_seconds', 0)
@@ -269,6 +276,10 @@ class RotaryPhone:
config["volume"] = SYS_CONFIG['system']['volume']
if "greeting_delay" not in config:
config["greeting_delay"] = SYS_CONFIG['system'].get('greeting_delay_seconds', 0)
if "beep_enabled" not in config:
config["beep_enabled"] = SYS_CONFIG['system'].get('beep_enabled', True)
if "beep_sound" not in config:
config["beep_sound"] = SYS_CONFIG['system'].get('beep_sound', 'beep.wav')
return config
except:
pass
@@ -300,6 +311,25 @@ class RotaryPhone:
self.config["extra_button_sound"] = filename
self.save_config()
def get_beep_sound_path(self):
"""Get the full path to the beep sound file"""
sound = self.config.get("beep_sound", "beep.wav")
return os.path.join(SOUNDS_DIR, sound)
def set_beep_sound(self, filename):
"""Set which sound plays as recording beep"""
self.config["beep_sound"] = filename
self.save_config()
def is_beep_enabled(self):
"""Check if beep sound is enabled"""
return self.config.get("beep_enabled", True)
def set_beep_enabled(self, enabled):
"""Enable or disable beep sound"""
self.config["beep_enabled"] = bool(enabled)
self.save_config()
def set_volume(self, volume):
"""Set playback volume (0-100)"""
volume = max(0, min(100, int(volume))) # Clamp between 0-100
@@ -345,7 +375,31 @@ class RotaryPhone:
wf.writeframes(tone.tobytes())
wf.close()
print(f"Default dial tone saved to {DIALTONE_FILE}")
def generate_default_beep(self):
"""Generate a simple beep tone (1000Hz) to indicate recording start"""
print("Generating default beep sound...")
beep_file = os.path.join(SOUNDS_DIR, "beep.wav")
duration = 0.5 # Half second beep
sample_rate = 44100
t = np.linspace(0, duration, int(sample_rate * duration), False)
# Generate 1000Hz tone
frequency = 1000
tone = np.sin(2 * np.pi * frequency * t)
# Convert to 16-bit PCM
tone = (tone * 32767).astype(np.int16)
# Save as WAV file
wf = wave.open(beep_file, 'wb')
wf.setnchannels(1)
wf.setsampwidth(2) # 16-bit
wf.setframerate(sample_rate)
wf.writeframes(tone.tobytes())
wf.close()
print(f"Default beep sound saved to {beep_file}")
def play_sound_file(self, filepath, check_hook_status=True):
"""Play a WAV file with volume control
@@ -585,6 +639,12 @@ class RotaryPhone:
greeting_file = self.get_active_greeting_path()
self.play_sound_file(greeting_file)
# Play beep sound to indicate recording will start
if self.phone_status == "off_hook" and self.is_beep_enabled():
beep_file = self.get_beep_sound_path()
if os.path.exists(beep_file):
self.play_sound_file(beep_file)
# Start recording
if self.phone_status == "off_hook": # Still off hook after sound
self.record_audio()
@@ -616,6 +676,8 @@ def index():
status = phone.get_status()
active_greeting = phone.config.get("active_greeting", "dialtone.wav")
extra_button_sound = phone.config.get("extra_button_sound", "button_sound.wav")
beep_sound = phone.config.get("beep_sound", "beep.wav")
beep_enabled = phone.is_beep_enabled()
volume = phone.get_volume()
greeting_delay = phone.get_greeting_delay()
@@ -624,6 +686,8 @@ def index():
greetings=greetings,
active_greeting=active_greeting,
extra_button_sound=extra_button_sound,
beep_sound=beep_sound,
beep_enabled=beep_enabled,
extra_button_enabled=EXTRA_BUTTON_ENABLED,
status=status,
volume=volume,
@@ -763,6 +827,33 @@ def set_extra_button_sound():
phone.set_extra_button_sound(filename)
return jsonify({"success": True, "message": f"Extra button sound set to '{filename}'"})
@app.route('/set_beep_sound', methods=['POST'])
def set_beep_sound():
"""Set which sound plays as recording beep"""
data = request.get_json()
filename = data.get('filename')
if not filename:
return jsonify({"error": "No filename provided"}), 400
filepath = os.path.join(SOUNDS_DIR, filename)
if not os.path.exists(filepath):
return jsonify({"error": "Sound file not found"}), 404
phone.set_beep_sound(filename)
return jsonify({"success": True, "message": f"Beep sound set to '{filename}'"})
@app.route('/api/beep_enabled', methods=['GET', 'POST'])
def beep_enabled():
"""Get or set beep enabled status"""
if request.method == 'POST':
data = request.get_json()
enabled = data.get('enabled', True)
phone.set_beep_enabled(enabled)
return jsonify({"success": True, "enabled": phone.is_beep_enabled()})
else:
return jsonify({"enabled": phone.is_beep_enabled()})
@app.route('/delete_greeting/<filename>', methods=['POST'])
def delete_greeting(filename):
"""Delete a greeting file"""
@@ -883,7 +974,8 @@ def get_greetings():
"date": datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M:%S'),
"duration": duration,
"is_active": filename == phone.config.get("active_greeting", "dialtone.wav"),
"is_button_sound": filename == phone.config.get("extra_button_sound", "button_sound.wav")
"is_button_sound": filename == phone.config.get("extra_button_sound", "button_sound.wav"),
"is_beep_sound": filename == phone.config.get("beep_sound", "beep.wav")
})
return greetings
@@ -1432,7 +1524,7 @@ def main():
</div>
<!-- Greeting Delay Control -->
<div style="padding: 20px;">
<div style="padding: 20px; border-bottom: 1px solid #e5e7eb;">
<h3 style="margin: 0 0 15px 0; font-size: 1.1em; color: #333;">Greeting Delay</h3>
<div style="display: flex; align-items: center; gap: 20px;">
<span style="font-size: 1.5em;">⏱️</span>
@@ -1445,6 +1537,22 @@ def main():
Delay before greeting plays after pickup (0-10 seconds)
</p>
</div>
<!-- Recording Beep Control -->
<div style="padding: 20px;">
<h3 style="margin: 0 0 15px 0; font-size: 1.1em; color: #333;">Recording Beep</h3>
<div style="display: flex; align-items: center; gap: 15px; justify-content: center;">
<span style="font-size: 1.5em;">📣</span>
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
<input type="checkbox" id="beep-enabled" {% if beep_enabled %}checked{% endif %}
style="width: 20px; height: 20px; cursor: pointer;">
<span style="font-size: 1em; font-weight: 500;">Play beep before recording</span>
</label>
</div>
<p style="margin-top: 10px; color: #6b7280; font-size: 0.85em; text-align: center;">
Audio cue to signal recording has started
</p>
</div>
</div>
<!-- Greeting Messages Card -->
@@ -1511,6 +1619,11 @@ def main():
🔘 Set Button
</button>
{% endif %}
{% if not greeting.is_beep_sound %}
<button class="btn btn-primary" onclick="setBeepSound('{{ greeting.filename }}')">
📣 Set Beep
</button>
{% endif %}
<button class="btn btn-danger" onclick="deleteGreeting('{{ greeting.filename }}', {{ loop.index }})">
🗑️ Delete
</button>
@@ -1523,6 +1636,11 @@ def main():
🔘 Set Button
</button>
{% endif %}
{% if not greeting.is_beep_sound %}
<button class="btn btn-primary" onclick="setBeepSound('{{ greeting.filename }}')">
📣 Set Beep
</button>
{% endif %}
{% endif %}
</div>
</div>
@@ -1715,6 +1833,25 @@ def main():
.catch(error => console.error('Error updating delay:', error));
}
// Beep enabled checkbox
const beepCheckbox = document.getElementById('beep-enabled');
beepCheckbox.addEventListener('change', function() {
fetch('/api/beep_enabled', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ enabled: this.checked })
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert(data.enabled ? 'Recording beep enabled ✓' : 'Recording beep disabled', 'success');
}
})
.catch(error => console.error('Error updating beep setting:', error));
});
function handleFileSelect(input) {
if (input.files && input.files.length > 0) {
selectedFiles = input.files;
@@ -1866,6 +2003,26 @@ def main():
.catch(error => showAlert('Error: ' + error, 'error'));
}
function setBeepSound(filename) {
fetch('/set_beep_sound', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ filename: filename })
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert(`Beep sound set to '${filename}'! ✓`, 'success');
setTimeout(() => location.reload(), 1500);
} else {
showAlert('Error: ' + data.error, 'error');
}
})
.catch(error => showAlert('Error: ' + error, 'error'));
}
function deleteGreeting(filename, index) {
if (confirm(`Delete greeting '${filename}'?`)) {
fetch('/delete_greeting/' + filename, { method: 'POST' })