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": {
|
"system": {
|
||||||
"active_greeting": "dialtone.wav",
|
"active_greeting": "dialtone.wav",
|
||||||
"extra_button_sound": "button_sound.wav",
|
"extra_button_sound": "button_sound.wav",
|
||||||
|
"beep_sound": "beep.wav",
|
||||||
|
"beep_enabled": true,
|
||||||
"greeting_delay_seconds": 0,
|
"greeting_delay_seconds": 0,
|
||||||
"volume": 70
|
"volume": 70
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ BACKUP_ON_WRITE = BACKUP_CONFIG.get('backup_on_write', True)
|
|||||||
WEB_PORT = SYS_CONFIG['web']['port']
|
WEB_PORT = SYS_CONFIG['web']['port']
|
||||||
|
|
||||||
# Template version - increment this when HTML template changes
|
# 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
|
# Flask app
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
@@ -250,11 +250,18 @@ class RotaryPhone:
|
|||||||
if not os.path.exists(DIALTONE_FILE):
|
if not os.path.exists(DIALTONE_FILE):
|
||||||
self.generate_default_dialtone()
|
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):
|
def load_config(self):
|
||||||
"""Load user runtime configuration from JSON file"""
|
"""Load user runtime configuration from JSON file"""
|
||||||
default_config = {
|
default_config = {
|
||||||
"active_greeting": SYS_CONFIG['system']['active_greeting'],
|
"active_greeting": SYS_CONFIG['system']['active_greeting'],
|
||||||
"extra_button_sound": SYS_CONFIG['system'].get('extra_button_sound', 'button_sound.wav'),
|
"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": [],
|
"greetings": [],
|
||||||
"volume": SYS_CONFIG['system']['volume'],
|
"volume": SYS_CONFIG['system']['volume'],
|
||||||
"greeting_delay": SYS_CONFIG['system'].get('greeting_delay_seconds', 0)
|
"greeting_delay": SYS_CONFIG['system'].get('greeting_delay_seconds', 0)
|
||||||
@@ -269,6 +276,10 @@ class RotaryPhone:
|
|||||||
config["volume"] = SYS_CONFIG['system']['volume']
|
config["volume"] = SYS_CONFIG['system']['volume']
|
||||||
if "greeting_delay" not in config:
|
if "greeting_delay" not in config:
|
||||||
config["greeting_delay"] = SYS_CONFIG['system'].get('greeting_delay_seconds', 0)
|
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
|
return config
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
@@ -300,6 +311,25 @@ class RotaryPhone:
|
|||||||
self.config["extra_button_sound"] = filename
|
self.config["extra_button_sound"] = filename
|
||||||
self.save_config()
|
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):
|
def set_volume(self, volume):
|
||||||
"""Set playback volume (0-100)"""
|
"""Set playback volume (0-100)"""
|
||||||
volume = max(0, min(100, int(volume))) # Clamp between 0-100
|
volume = max(0, min(100, int(volume))) # Clamp between 0-100
|
||||||
@@ -346,6 +376,30 @@ class RotaryPhone:
|
|||||||
wf.close()
|
wf.close()
|
||||||
print(f"Default dial tone saved to {DIALTONE_FILE}")
|
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):
|
def play_sound_file(self, filepath, check_hook_status=True):
|
||||||
"""Play a WAV file with volume control
|
"""Play a WAV file with volume control
|
||||||
|
|
||||||
@@ -585,6 +639,12 @@ class RotaryPhone:
|
|||||||
greeting_file = self.get_active_greeting_path()
|
greeting_file = self.get_active_greeting_path()
|
||||||
self.play_sound_file(greeting_file)
|
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
|
# Start recording
|
||||||
if self.phone_status == "off_hook": # Still off hook after sound
|
if self.phone_status == "off_hook": # Still off hook after sound
|
||||||
self.record_audio()
|
self.record_audio()
|
||||||
@@ -616,6 +676,8 @@ def index():
|
|||||||
status = phone.get_status()
|
status = phone.get_status()
|
||||||
active_greeting = phone.config.get("active_greeting", "dialtone.wav")
|
active_greeting = phone.config.get("active_greeting", "dialtone.wav")
|
||||||
extra_button_sound = phone.config.get("extra_button_sound", "button_sound.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()
|
volume = phone.get_volume()
|
||||||
greeting_delay = phone.get_greeting_delay()
|
greeting_delay = phone.get_greeting_delay()
|
||||||
|
|
||||||
@@ -624,6 +686,8 @@ def index():
|
|||||||
greetings=greetings,
|
greetings=greetings,
|
||||||
active_greeting=active_greeting,
|
active_greeting=active_greeting,
|
||||||
extra_button_sound=extra_button_sound,
|
extra_button_sound=extra_button_sound,
|
||||||
|
beep_sound=beep_sound,
|
||||||
|
beep_enabled=beep_enabled,
|
||||||
extra_button_enabled=EXTRA_BUTTON_ENABLED,
|
extra_button_enabled=EXTRA_BUTTON_ENABLED,
|
||||||
status=status,
|
status=status,
|
||||||
volume=volume,
|
volume=volume,
|
||||||
@@ -763,6 +827,33 @@ def set_extra_button_sound():
|
|||||||
phone.set_extra_button_sound(filename)
|
phone.set_extra_button_sound(filename)
|
||||||
return jsonify({"success": True, "message": f"Extra button sound set to '{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'])
|
@app.route('/delete_greeting/<filename>', methods=['POST'])
|
||||||
def delete_greeting(filename):
|
def delete_greeting(filename):
|
||||||
"""Delete a greeting file"""
|
"""Delete a greeting file"""
|
||||||
@@ -883,7 +974,8 @@ def get_greetings():
|
|||||||
"date": datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M:%S'),
|
"date": datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
"duration": duration,
|
"duration": duration,
|
||||||
"is_active": filename == phone.config.get("active_greeting", "dialtone.wav"),
|
"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
|
return greetings
|
||||||
|
|
||||||
@@ -1432,7 +1524,7 @@ def main():
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Greeting Delay Control -->
|
<!-- 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>
|
<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;">
|
<div style="display: flex; align-items: center; gap: 20px;">
|
||||||
<span style="font-size: 1.5em;">⏱️</span>
|
<span style="font-size: 1.5em;">⏱️</span>
|
||||||
@@ -1445,6 +1537,22 @@ def main():
|
|||||||
Delay before greeting plays after pickup (0-10 seconds)
|
Delay before greeting plays after pickup (0-10 seconds)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Greeting Messages Card -->
|
<!-- Greeting Messages Card -->
|
||||||
@@ -1511,6 +1619,11 @@ def main():
|
|||||||
🔘 Set Button
|
🔘 Set Button
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% 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 }})">
|
<button class="btn btn-danger" onclick="deleteGreeting('{{ greeting.filename }}', {{ loop.index }})">
|
||||||
🗑️ Delete
|
🗑️ Delete
|
||||||
</button>
|
</button>
|
||||||
@@ -1523,6 +1636,11 @@ def main():
|
|||||||
🔘 Set Button
|
🔘 Set Button
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if not greeting.is_beep_sound %}
|
||||||
|
<button class="btn btn-primary" onclick="setBeepSound('{{ greeting.filename }}')">
|
||||||
|
📣 Set Beep
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1715,6 +1833,25 @@ def main():
|
|||||||
.catch(error => console.error('Error updating delay:', error));
|
.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) {
|
function handleFileSelect(input) {
|
||||||
if (input.files && input.files.length > 0) {
|
if (input.files && input.files.length > 0) {
|
||||||
selectedFiles = input.files;
|
selectedFiles = input.files;
|
||||||
@@ -1866,6 +2003,26 @@ def main():
|
|||||||
.catch(error => showAlert('Error: ' + error, 'error'));
|
.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) {
|
function deleteGreeting(filename, index) {
|
||||||
if (confirm(`Delete greeting '${filename}'?`)) {
|
if (confirm(`Delete greeting '${filename}'?`)) {
|
||||||
fetch('/delete_greeting/' + filename, { method: 'POST' })
|
fetch('/delete_greeting/' + filename, { method: 'POST' })
|
||||||
|
|||||||
Reference in New Issue
Block a user