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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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' })
|
||||
|
||||
Reference in New Issue
Block a user