ASSISTANT
Creating an advanced voice/meeting recorder using Python and Flask involves several components, including the backend server, frontend interface, audio recording functionalities, and database management to store recordings, notes, and segments. Below is a comprehensive guide to building such an application.
## Overview
1. **Backend (Flask):**
- Handle routes for recording, viewing recordings, saving notes, and segments.
- Manage audio recording using Python libraries.
- Interact with a SQLite database to store metadata.
2. **Frontend (HTML/CSS/JavaScript):**
- Provide a user interface with tabs for recording and viewing older recordings.
- Include controls for starting/stopping recordings, selecting audio sources, adding titles, notes, and segments.
3. **Database (SQLite):**
- Store information about each recording, including title, timestamp, notes, segments, and file paths.
## Prerequisites
Before proceeding, ensure you have the following installed:
- Python 3.x
- Flask
- SQLite
- Necessary Python libraries (`pyaudio`, `wave`, etc.)
You can install the required Python libraries using:
```bash
pip install Flask pyaudio
```
## Project Structure
```
meeting_recorder/
│
├── app.py
├── recordings.db
├── static/
│ └── styles.css
└── templates/
├── base.html
├── index.html
└── recordings.html
```
## Step-by-Step Implementation
### 1. Setting Up the Flask Application (`app.py`)
```python
import os
import threading
import wave
import datetime
from flask import Flask, render_template, request, redirect, url_for, jsonify
import sqlite3
import pyaudio
app = Flask(__name__)
DATABASE = 'recordings.db'
AUDIO_FOLDER = 'recordings'
if not os.path.exists(AUDIO_FOLDER):
os.makedirs(AUDIO_FOLDER)
# Initialize the database
def init_db():
conn = sqlite3.connect(DATABASE)
c = conn.cursor()
c.execute('''CREATE TABLE IF NOT EXISTS recordings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT,
filename TEXT,
start_time TEXT,
end_time TEXT
)''')
c.execute('''CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
recording_id INTEGER,
note_text TEXT,
timestamp TEXT,
FOREIGN KEY(recording_id) REFERENCES recordings(id)
)''')
c.execute('''CREATE TABLE IF NOT EXISTS segments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
recording_id INTEGER,
segment_name TEXT,
timestamp TEXT,
FOREIGN KEY(recording_id) REFERENCES recordings(id)
)''')
conn.commit()
conn.close()
init_db()
# Global variables to control recording
is_recording = False
recording_thread = None
current_filename = ""
start_time = None
end_time = None
def record_audio(filename, audio_source):
global is_recording, start_time, end_time
CHUNK = 1024
FORMAT = pyaudio.paInt16
CHANNELS = 2 if audio_source == 'both' else 1
RATE = 44100
p = pyaudio.PyAudio()
if audio_source == 'microphone':
stream = p.open(format=FORMAT,
channels=CHANNELS,
rate=RATE,
input=True,
frames_per_buffer=CHUNK)
elif audio_source == 'desktop':
# This requires virtual audio cable or similar setup
# Implementation depends on the OS and may require additional setup
# Placeholder for desktop audio
stream = p.open(format=FORMAT,
channels=CHANNELS,
rate=RATE,
input=True,
frames_per_buffer=CHUNK)
else: # both
stream = p.open(format=FORMAT,
channels=CHANNELS,
rate=RATE,
input=True,
frames_per_buffer=CHUNK)
frames = []
start_time = datetime.datetime.now().strftime('%H:%M')
while is_recording:
data = stream.read(CHUNK)
frames.append(data)
end_time = datetime.datetime.now().strftime('%H:%M')
stream.stop_stream()
stream.close()
p.terminate()
wf = wave.open(os.path.join(AUDIO_FOLDER, filename), 'wb')
wf.setnchannels(CHANNELS)
wf.setsampwidth(p.get_sample_size(FORMAT))
wf.setframerate(RATE)
wf.writeframes(b''.join(frames))
wf.close()
@app.route('/')
def index():
return render_template('index.html')
@app.route('/start_record', methods=['POST'])
def start_record():
global is_recording, recording_thread, current_filename
if not is_recording:
data = request.json
title = data.get('title', f"Recording_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}")
audio_source = data.get('audio_source', 'both')
current_filename = f"{title}_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.wav"
is_recording = True
recording_thread = threading.Thread(target=record_audio, args=(current_filename, audio_source))
recording_thread.start()
# Save initial recording entry to DB
conn = sqlite3.connect(DATABASE)
c = conn.cursor()
c.execute("INSERT INTO recordings (title, filename, start_time) VALUES (?, ?, ?)",
(title, current_filename, datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
conn.commit()
conn.close()
return jsonify({"status": "started"})
else:
return jsonify({"status": "already recording"})
@app.route('/stop_record', methods=['POST'])
def stop_record():
global is_recording
if is_recording:
is_recording = False
recording_thread.join()
# Update end_time in DB
conn = sqlite3.connect(DATABASE)
c = conn.cursor()
c.execute("UPDATE recordings SET end_time = ? WHERE filename = ?",
(end_time, current_filename))
conn.commit()
conn.close()
return jsonify({"status": "stopped"})
else:
return jsonify({"status": "not recording"})
@app.route('/save_notes', methods=['POST'])
def save_notes():
data = request.json
recording_id = data.get('recording_id')
note_text = data.get('note_text')
timestamp = data.get('timestamp') # Should be in format 'HH:MM'
conn = sqlite3.connect(DATABASE)
c = conn.cursor()
c.execute("INSERT INTO notes (recording_id, note_text, timestamp) VALUES (?, ?, ?)",
(recording_id, note_text, timestamp))
conn.commit()
conn.close()
return jsonify({"status": "note saved"})
@app.route('/add_segment', methods=['POST'])
def add_segment():
data = request.json
recording_id = data.get('recording_id')
segment_name = data.get('segment_name')
timestamp = data.get('timestamp') # Should be in format 'HH:MM'
conn = sqlite3.connect(DATABASE)
c = conn.cursor()
c.execute("INSERT INTO segments (recording_id, segment_name, timestamp) VALUES (?, ?, ?)",
(recording_id, segment_name, timestamp))
conn.commit()
conn.close()
return jsonify({"status": "segment added"})
@app.route('/recordings')
def view_recordings():
conn = sqlite3.connect(DATABASE)
c = conn.cursor()
c.execute("SELECT * FROM recordings ORDER BY id DESC")
recordings = c.fetchall()
recording_list = []
for rec in recordings:
rec_id, title, filename, start, end = rec
c.execute("SELECT * FROM notes WHERE recording_id = ?", (rec_id,))
notes = c.fetchall()
c.execute("SELECT * FROM segments WHERE recording_id = ?", (rec_id,))
segments = c.fetchall()
recording_list.append({
'id': rec_id,
'title': title,
'filename': filename,
'start_time': start,
'end_time': end,
'notes': notes,
'segments': segments
})
conn.close()
return render_template('recordings.html', recordings=recording_list)
@app.route('/download/<filename>')
def download_file(filename):
return redirect(url_for('static', filename=f"{AUDIO_FOLDER}/{filename}"))
if __name__ == '__main__':
app.run(debug=True)
```
### 2. Creating HTML Templates
#### a. Base Template (`templates/base.html`)
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Advanced Meeting Recorder</title>
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<div class="navbar">
<a href="{{ url_for('index') }}">Record</a>
<a href="{{ url_for('view_recordings') }}">Recordings</a>
</div>
<div class="container">
{% block content %}{% endblock %}
</div>
</body>
</html>
```
#### b. Recording Page (`templates/index.html`)
```html
{% extends "base.html" %}
{% block content %}
<h1>Meeting Recorder</h1>
<div class="record-controls">
<label for="audio_source">Audio Source:</label>
<select id="audio_source">
<option value="microphone">Microphone</option>
<option value="desktop">Desktop Audio</option>
<option value="both" selected>Both</option>
</select>
</div>
<div class="title-input">
<label for="title">Title:</label>
<input type="text" id="title" placeholder="Enter meeting title">
</div>
<div class="buttons">
<button id="start_btn">Start Recording</button>
<button id="stop_btn" disabled>Stop Recording</button>
</div>
<div class="notes-section">
<h2>Notes</h2>
<textarea id="notes" placeholder="Take your notes here..."></textarea>
<button id="save_note_btn">Save Note</button>
</div>
<div class="segments-section">
<h2>Segments</h2>
<input type="text" id="segment_name" placeholder="Segment name">
<button id="add_segment_btn">Add Segment</button>
</div>
<script>
let recordingId = null;
$('#start_btn').click(function(){
const title = $('#title').val() || 'Untitled';
const audio_source = $('#audio_source').val();
$.ajax({
url: '/start_record',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({title: title, audio_source: audio_source}),
success: function(response){
if(response.status === 'started'){
$('#start_btn').prop('disabled', true);
$('#stop_btn').prop('disabled', false);
}
// Fetch the latest recording ID
$.ajax({
url: '/get_latest_recording_id',
type: 'GET',
success: function(res){
recordingId = res.id;
}
});
}
});
});
$('#stop_btn').click(function(){
$.ajax({
url: '/stop_record',
type: 'POST',
success: function(response){
if(response.status === 'stopped'){
$('#start_btn').prop('disabled', false);
$('#stop_btn').prop('disabled', true);
}
}
});
});
$('#save_note_btn').click(function(){
const note_text = $('#notes').val();
const timestamp = new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
if(recordingId && note_text){
$.ajax({
url: '/save_notes',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({
recording_id: recordingId,
note_text: note_text,
timestamp: timestamp
}),
success: function(response){
if(response.status === 'note saved'){
alert('Note saved!');
$('#notes').val('');
}
}
});
}
});
$('#add_segment_btn').click(function(){
const segment_name = $('#segment_name').val();
const timestamp = new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
if(recordingId && segment_name){
$.ajax({
url: '/add_segment',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({
recording_id: recordingId,
segment_name: segment_name,
timestamp: timestamp
}),
success: function(response){
if(response.status === 'segment added'){
alert('Segment added!');
$('#segment_name').val('');
}
}
});
}
});
</script>
{% endblock %}
```
#### c. View Recordings Page (`templates/recordings.html`)
```html
{% extends "base.html" %}
{% block content %}
<h1>Recorded Meetings</h1>
{% for recording in recordings %}
<div class="recording">
<h2>{{ recording.title }}</h2>
<p><strong>Time:</strong> {{ recording.start_time }} - {{ recording.end_time }}</p>
<audio controls>
<source src="{{ url_for('download_file', filename=recording.filename) }}" type="audio/wav">
Your browser does not support the audio element.
</audio>
<div class="segments">
<h3>Segments</h3>
<ul>
{% for segment in recording.segments %}
<li>{{ segment[3] }} - {{ segment[2] }}</li>
{% endfor %}
</ul>
</div>
<div class="notes">
<h3>Notes</h3>
<ul>
{% for note in recording.notes %}
<li>{{ note[3] }} - {{ note[2] }}</li>
{% endfor %}
</ul>
</div>
</div>
<hr>
{% endfor %}
{% endblock %}
```
### 3. Adding Styles (`static/styles.css`)
```css
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
}
.navbar {
background-color: #333;
overflow: hidden;
}
.navbar a {
float: left;
color: #f2f2f2;
text-align: center;
padding: 14px 16px;
text-decoration: none;
}
.navbar a:hover {
background-color: #ddd;
color: black;
}
.container {
padding: 20px;
}
.record-controls, .title-input, .buttons, .notes-section, .segments-section {
margin-bottom: 20px;
}
.recording {
margin-bottom: 30px;
}
.audio-player {
margin-top: 10px;
}
```
### 4. Handling Latest Recording ID Endpoint
To fetch the latest recording ID after starting a recording, add the following route to `app.py`:
```python
@app.route('/get_latest_recording_id', methods=['GET'])
def get_latest_recording_id():
conn = sqlite3.connect(DATABASE)
c = conn.cursor()
c.execute("SELECT id FROM recordings ORDER BY id DESC LIMIT 1")
rec = c.fetchone()
conn.close()
if rec:
return jsonify({"id": rec[0]})
else:
return jsonify({"id": None})
```
### 5. Serving Audio Files
Ensure Flask can serve audio files by placing them in a static folder or configuring the static route appropriately. In the `app.py`, the `/download/<filename>` route redirects to the static files. Make sure the `recordings` folder is within the `static` directory or adjust the path accordingly.
### 6. Running the Application
Navigate to your project directory and run:
```bash
python app.py
```
Access the application by navigating to `http://127.0.0.1:5000/` in your web browser.
## Notes and Considerations
1. **Audio Source Selection:**
- Recording desktop audio may require additional setup depending on the operating system. Tools like Virtual Audio Cable (Windows) or `pulseaudio` (Linux) might be necessary.
- The provided code includes placeholders for desktop audio. Implementing desktop audio recording is beyond the scope of this guide and may require platform-specific solutions.
2. **Concurrency and Multiple Users:**
- The current implementation is suitable for a single-user local environment. For multi-user scenarios, additional management for recording sessions and user authentication would be required.
3. **Error Handling:**
- For production use, enhance error handling to manage exceptions during recording, database operations, and file I/O.
4. **Security:**
- Ensure that the file upload/download mechanisms are secure to prevent unauthorized access or code injection.
5. **Enhancements:**
- Implement real-time updates for notes and segments.
- Add user authentication and authorization.
- Provide search and filter functionalities for recordings.
## Conclusion
This guide provides a foundational structure for building an advanced voice/meeting recorder using Python and Flask. While it covers the core functionalities, further enhancements and platform-specific implementations may be necessary to tailor the application to specific needs and environments.