settings work

This commit is contained in:
michalcourson
2026-02-22 14:57:04 -05:00
parent f2718282c7
commit d49ac95fa2
16 changed files with 205 additions and 103 deletions

View File

@ -1,10 +1,16 @@
{
"input_device": {
"default_samplerate": 44100.0,
"index": 1,
"max_input_channels": 8,
"name": "VM Mic mix (VB-Audio Voicemeete"
"index": 49,
"name": "Microphone (Logi C615 HD WebCam)",
"max_input_channels": 1,
"default_samplerate": 48000.0
},
"save_path": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings",
"recording_length": 30
"recording_length": 30,
"output_device": {
"index": 40,
"name": "Speakers (Realtek(R) Audio)",
"max_output_channels": 2,
"default_samplerate": 48000.0
}
}

View File

@ -30,7 +30,6 @@ class AudioRecorder:
self.recordings_dir = "recordings"
self.stream = sd.InputStream(
samplerate=self.sample_rate,
callback=self.record_callback
)
@ -45,7 +44,6 @@ class AudioRecorder:
self.buffer = np.zeros((int(self.duration * self.sample_rate), self.channels), dtype=np.float32)
print(f"AudioRecorder initialized with duration={self.duration}s, sample_rate={self.sample_rate}Hz, channels={self.channels}")
self.stream = sd.InputStream(
samplerate=self.sample_rate,
callback=self.record_callback
)

View File

@ -19,13 +19,13 @@ recorder = AudioRecorder()
# except Exception as e:
# return jsonify({'status': 'error', 'message': str(e)}), 400
@device_bp.route('/device/get', methods=['GET'])
def get_audio_device():
try:
device_info = audio_manager.get_default_device('input')
return jsonify({'status': 'success', 'device_info': device_info})
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 400
# @device_bp.route('/device/get', methods=['GET'])
# def get_audio_device():
# try:
# device_info = audio_manager.get_default_device('input')
# return jsonify({'status': 'success', 'device_info': device_info})
# except Exception as e:
# return jsonify({'status': 'error', 'message': str(e)}), 400
@device_bp.route('/device/list', methods=['GET'])
def list_audio_devices():

View File

@ -20,21 +20,44 @@ class SettingsManager:
else:
self.settings = {
"input_device": None,
"output_device": None,
"save_path": os.path.join(os.getcwd(), "recordings"),
"recording_length": 15
}
audio_manager = WindowsAudioManager()
devices = audio_manager.list_audio_devices('input')
print(f"Available input devices: {self.settings}")
input = self.settings["input_device"]
input_devices = audio_manager.list_audio_devices('input')
output_devices = audio_manager.list_audio_devices('output')
# print("Available input devices:")
# for i, dev in enumerate(input_devices):
# print(i, dev['name'])
# print("Available output devices:")
# for i, dev in enumerate(output_devices):
# print(i, dev['name'])
# print(f"Available input devices: {input_devices}")
# print(f"Available output devices: {output_devices}")
input = None
output = None
if("input_device" in self.settings):
input = self.settings["input_device"]
if("output_device" in self.settings):
output = self.settings["output_device"]
#see if input device is in "devices", if not set to the first index
if input is not None and any(d['name'] == input["name"] for d in devices):
if input is not None and any(d['name'] == input["name"] for d in input_devices):
print(f"Using saved input device index: {input}")
else:
input = devices[0] if devices else None
input = input_devices[0] if input_devices else None
self.settings["input_device"] = input
#see if output device is in "devices", if not set to the first index
if output is not None and any(d['name'] == output["name"] for d in output_devices):
print(f"Using saved output device index: {output}")
else:
output = output_devices[0] if output_devices else None
self.settings["output_device"] = output
self.save_settings()

View File

@ -18,7 +18,19 @@ class WindowsAudioManager:
"""
Initialize Windows audio device and volume management.
"""
self.devices = sd.query_devices()
host_apis = sd.query_hostapis()
wasapi_device_indexes = None
for api in host_apis:
if api['name'].lower() == 'Windows WASAPI'.lower():
wasapi_device_indexes = api['devices']
break
# print(f"Host APIs: {host_apis}")
print(f"WASAPI Device Indexes: {wasapi_device_indexes}")
wasapi_device_indexes = set(wasapi_device_indexes) if wasapi_device_indexes is not None else set()
self.devices = [dev for dev in sd.query_devices() if dev['index'] in wasapi_device_indexes]
# self.devices = sd.query_devices()
print(f"devices: {self.devices}")
self.default_input = sd.default.device[0]
self.default_output = sd.default.device[1]
@ -34,7 +46,7 @@ class WindowsAudioManager:
{
'index': dev['index'],
'name': dev['name'],
'max_input_channels': dev['max_input_channels'],
'channels': dev['max_input_channels'],
'default_samplerate': dev['default_samplerate']
}
for dev in self.devices if dev['max_input_channels'] > 0
@ -44,7 +56,7 @@ class WindowsAudioManager:
{
'index': dev['index'],
'name': dev['name'],
'max_output_channels': dev['max_output_channels'],
'channels': dev['max_output_channels'],
'default_samplerate': dev['default_samplerate']
}
for dev in self.devices if dev['max_output_channels'] > 0

View File

View File

@ -14,6 +14,7 @@ import { useAppDispatch, useAppSelector } from './hooks';
import { store } from '../redux/main';
import { useNavigate } from 'react-router-dom';
import SettingsPage from './Settings';
import { apiFetch } from './api';
function MainPage() {
const dispatch = useAppDispatch();
@ -30,7 +31,7 @@ function MainPage() {
useEffect(() => {
const fetchMetadata = async () => {
try {
const response = await fetch('http://localhost:5010/meta');
const response = await apiFetch('meta');
const data = await response.json();
dispatch({ type: 'metadata/setAllData', payload: data });
} catch (error) {

View File

@ -5,38 +5,57 @@ import './App.css';
import TextField from '@mui/material/TextField';
import Select from '@mui/material/Select';
import MenuItem from '@mui/material/MenuItem';
import { apiFetch } from './api';
type AudioDevice = {
id: string;
label: string;
index: number;
name: string;
default_sample_rate: number;
channels: number;
};
type Settings = {
httpPort: string;
inputDevice: AudioDevice;
outputDevice: AudioDevice;
recordingLength: number;
outputFolder: string;
http_port: number;
input_device: AudioDevice;
output_device: AudioDevice;
recording_length: number;
save_path: string;
};
const defaultSettings: Settings = {
httpPort: '',
inputDevice: { id: '', label: '' },
outputDevice: { id: '', label: '' },
recordingLength: 0,
outputFolder: '',
http_port: 0,
input_device: { index: 0, name: '', default_sample_rate: 0, channels: 0 },
output_device: { index: 0, name: '', default_sample_rate: 0, channels: 0 },
recording_length: 0,
save_path: '',
};
const fetchAudioDevices = async (): Promise<AudioDevice[]> => {
async function fetchAudioDevices(
type: 'input' | 'output',
): Promise<AudioDevice[]> {
// Replace with actual backend call
// Example: return window.api.getAudioDevices();
return [
{ id: 'default-in', label: 'Default Input' },
{ id: 'mic-1', label: 'Microphone 1' },
{ id: 'default-out', label: 'Default Output' },
{ id: 'spk-1', label: 'Speakers 1' },
];
};
return apiFetch(`device/list?device_type=${type}`)
.then((res) => res.json())
.then((data) => data.devices as AudioDevice[])
.catch((error) => {
console.error('Error fetching audio devices:', error);
return [];
});
}
async function fetchSettings(): Promise<Settings> {
// Replace with actual backend call
// Example: return window.api.getAudioDevices();
console.log('Fetching settings from backend...');
return apiFetch('settings')
.then((res) => res.json())
.then((data) => data.settings as Settings)
.catch((error) => {
console.error('Error fetching settings:', error);
return defaultSettings;
});
}
const sendSettingsToBackend = (settings: Settings) => {
// Replace with actual backend call
@ -51,11 +70,24 @@ export default function SettingsPage() {
const navigate = useNavigate();
useEffect(() => {
fetchAudioDevices()
fetchSettings()
.then((fetchedSettings) => {
console.log('Fetched settings:', fetchedSettings);
setSettings(fetchedSettings);
return null;
})
.then(() => {
return fetchAudioDevices('input');
})
.then((devices) => {
// For demo, split devices by id
setInputDevices(devices.filter((d) => d.id.includes('in')));
setOutputDevices(devices.filter((d) => d.id.includes('out')));
setInputDevices(devices);
// console.log('Input devices:', devices);
return fetchAudioDevices('output');
})
.then((devices) => {
setOutputDevices(devices);
// console.log('Output devices:', devices);
return devices;
})
.catch((error) => {
@ -78,16 +110,16 @@ export default function SettingsPage() {
const handleFolderChange = async () => {
// Replace with actual folder picker
// Example: const folder = await window.api.selectFolder();
const folder = window.prompt(
'Enter output folder path:',
settings.outputFolder,
);
if (folder !== null) {
setSettings((prev) => ({
...prev,
outputFolder: folder,
}));
}
// const folder = window.prompt(
// 'Enter output folder path:',
// settings.outputFolder,
// );
// if (folder !== null) {
// setSettings((prev) => ({
// ...prev,
// outputFolder: folder,
// }));
// }
};
return (
@ -109,9 +141,15 @@ export default function SettingsPage() {
variant="standard"
type="text"
name="httpPort"
value={settings.httpPort}
value={settings.http_port}
onBlur={() => console.log('port blur')}
onChange={(e) => {
setSettings((prev) => ({ ...prev, httpPort: e.target.value }));
if (!Number.isNaN(Number(e.target.value))) {
setSettings((prev) => ({
...prev,
http_port: Number(e.target.value),
}));
}
}}
className="ml-2 text-white w-[150px]"
/>
@ -121,15 +159,24 @@ export default function SettingsPage() {
<Select
variant="standard"
name="inputDevice"
value={settings.inputDevice}
value={settings.input_device.index}
onChange={(e) => {
setSettings((prev) => ({ ...prev, inputDevice: e.target.value }));
const newDevice = inputDevices.find(
(dev) => dev.index === Number(e.target.value),
);
console.log('Selected input device index:', newDevice);
if (newDevice) {
setSettings((prev) => ({
...prev,
inputDevice: newDevice,
}));
}
}}
className="ml-2 w-64"
>
{inputDevices.map((dev) => (
<MenuItem key={dev.id} value={dev.id}>
{dev.label}
<MenuItem key={dev.index} value={dev.index}>
{dev.name}
</MenuItem>
))}
</Select>
@ -139,18 +186,23 @@ export default function SettingsPage() {
<Select
variant="standard"
name="outputDevice"
value={settings.outputDevice}
value={settings.output_device.index}
onChange={(e) => {
setSettings((prev) => ({
...prev,
outputDevice: e.target.value,
}));
const newDevice = outputDevices.find(
(dev) => dev.index === Number(e.target.value),
);
if (newDevice) {
setSettings((prev) => ({
...prev,
output_device: newDevice,
}));
}
}}
className="ml-2 w-64"
>
{outputDevices.map((dev) => (
<MenuItem key={dev.id} value={dev.id}>
{dev.label}
<MenuItem key={dev.index} value={dev.index}>
{dev.name}
</MenuItem>
))}
</Select>
@ -161,12 +213,14 @@ export default function SettingsPage() {
variant="standard"
type="text"
name="recordingLength"
value={settings.recordingLength}
value={settings.recording_length}
onChange={(e) => {
setSettings((prev) => ({
...prev,
recordingLength: e.target.value,
}));
if (!Number.isNaN(Number(e.target.value))) {
setSettings((prev) => ({
...prev,
recording_length: Number(e.target.value),
}));
}
}}
className="ml-2 w-[150px]"
/>
@ -177,8 +231,8 @@ export default function SettingsPage() {
<TextField
variant="standard"
type="text"
name="outputFolder"
value={settings.outputFolder}
name="savePath"
value={settings.save_path}
className="ml-2 w-[300px]"
/>
<button

View File

@ -0,0 +1,13 @@
const getBaseUrl = () => {
// You can store the base URL in localStorage, a config file, or state
return localStorage.getItem('baseUrl') || 'http://localhost:5010';
};
export function apiFetch(endpoint: string, options = {}) {
const url = `${getBaseUrl()}/${endpoint}`;
return fetch(url, options);
}
export function setBaseUrl(baseUrl: string) {
localStorage.setItem('baseUrl', baseUrl);
}

View File

@ -15,6 +15,7 @@ import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
import AudioTrimmer from './AudioTrimer';
import { ClipMetadata } from '../../redux/types';
import { useAppDispatch, useAppSelector } from '../hooks';
import { apiFetch } from '../api';
export interface ClipListProps {
collection: string;
@ -77,19 +78,16 @@ export default function ClipList({ collection }: ClipListProps) {
payload: { collection, newMetadata },
});
try {
const response = await fetch(
'http://localhost:5010/meta/collection/clips/reorder',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: collection,
clips: newMetadata.clips,
}),
const response = await apiFetch('meta/collection/clips/reorder', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
);
body: JSON.stringify({
name: collection,
clips: newMetadata.clips,
}),
});
const data = await response.json();
console.log('handle reorder return:', data.collections);
dispatch({ type: 'metadata/setAllData', payload: data });
@ -105,7 +103,7 @@ export default function ClipList({ collection }: ClipListProps) {
type: 'metadata/deleteClip',
payload: { collection, clip: meta },
});
fetch('http://localhost:5010/meta/collection/clips/remove', {
apiFetch('meta/collection/clips/remove', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -126,7 +124,7 @@ export default function ClipList({ collection }: ClipListProps) {
type: 'metadata/moveClip',
payload: { sourceCollection: collection, targetCollection, clip: meta },
});
fetch('http://localhost:5010/meta/collection/clips/move', {
apiFetch('meta/collection/clips/move', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -147,19 +145,16 @@ export default function ClipList({ collection }: ClipListProps) {
type: 'metadata/editClip',
payload: { collection, clip: meta },
});
const response = await fetch(
'http://localhost:5010/meta/collection/clips/edit',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: collection,
clip: meta,
}),
const response = await apiFetch('meta/collection/clips/edit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
);
body: JSON.stringify({
name: collection,
clip: meta,
}),
});
await response.json();
// console.log('handle clip save return:', data.collections);
dispatch({