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": { "input_device": {
"default_samplerate": 44100.0, "index": 49,
"index": 1, "name": "Microphone (Logi C615 HD WebCam)",
"max_input_channels": 8, "max_input_channels": 1,
"name": "VM Mic mix (VB-Audio Voicemeete" "default_samplerate": 48000.0
}, },
"save_path": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings", "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.recordings_dir = "recordings"
self.stream = sd.InputStream( self.stream = sd.InputStream(
samplerate=self.sample_rate,
callback=self.record_callback 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) 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}") print(f"AudioRecorder initialized with duration={self.duration}s, sample_rate={self.sample_rate}Hz, channels={self.channels}")
self.stream = sd.InputStream( self.stream = sd.InputStream(
samplerate=self.sample_rate,
callback=self.record_callback callback=self.record_callback
) )

View File

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

View File

@ -20,21 +20,44 @@ class SettingsManager:
else: else:
self.settings = { self.settings = {
"input_device": None, "input_device": None,
"output_device": None,
"save_path": os.path.join(os.getcwd(), "recordings"), "save_path": os.path.join(os.getcwd(), "recordings"),
"recording_length": 15 "recording_length": 15
} }
audio_manager = WindowsAudioManager() audio_manager = WindowsAudioManager()
devices = audio_manager.list_audio_devices('input') input_devices = audio_manager.list_audio_devices('input')
print(f"Available input devices: {self.settings}") output_devices = audio_manager.list_audio_devices('output')
input = self.settings["input_device"] # 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 #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}") print(f"Using saved input device index: {input}")
else: else:
input = devices[0] if devices else None input = input_devices[0] if input_devices else None
self.settings["input_device"] = input 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() self.save_settings()

View File

@ -18,7 +18,19 @@ class WindowsAudioManager:
""" """
Initialize Windows audio device and volume management. 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_input = sd.default.device[0]
self.default_output = sd.default.device[1] self.default_output = sd.default.device[1]
@ -34,7 +46,7 @@ class WindowsAudioManager:
{ {
'index': dev['index'], 'index': dev['index'],
'name': dev['name'], 'name': dev['name'],
'max_input_channels': dev['max_input_channels'], 'channels': dev['max_input_channels'],
'default_samplerate': dev['default_samplerate'] 'default_samplerate': dev['default_samplerate']
} }
for dev in self.devices if dev['max_input_channels'] > 0 for dev in self.devices if dev['max_input_channels'] > 0
@ -44,7 +56,7 @@ class WindowsAudioManager:
{ {
'index': dev['index'], 'index': dev['index'],
'name': dev['name'], 'name': dev['name'],
'max_output_channels': dev['max_output_channels'], 'channels': dev['max_output_channels'],
'default_samplerate': dev['default_samplerate'] 'default_samplerate': dev['default_samplerate']
} }
for dev in self.devices if dev['max_output_channels'] > 0 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 { store } from '../redux/main';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import SettingsPage from './Settings'; import SettingsPage from './Settings';
import { apiFetch } from './api';
function MainPage() { function MainPage() {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -30,7 +31,7 @@ function MainPage() {
useEffect(() => { useEffect(() => {
const fetchMetadata = async () => { const fetchMetadata = async () => {
try { try {
const response = await fetch('http://localhost:5010/meta'); const response = await apiFetch('meta');
const data = await response.json(); const data = await response.json();
dispatch({ type: 'metadata/setAllData', payload: data }); dispatch({ type: 'metadata/setAllData', payload: data });
} catch (error) { } catch (error) {

View File

@ -5,38 +5,57 @@ import './App.css';
import TextField from '@mui/material/TextField'; import TextField from '@mui/material/TextField';
import Select from '@mui/material/Select'; import Select from '@mui/material/Select';
import MenuItem from '@mui/material/MenuItem'; import MenuItem from '@mui/material/MenuItem';
import { apiFetch } from './api';
type AudioDevice = { type AudioDevice = {
id: string; index: number;
label: string; name: string;
default_sample_rate: number;
channels: number;
}; };
type Settings = { type Settings = {
httpPort: string; http_port: number;
inputDevice: AudioDevice; input_device: AudioDevice;
outputDevice: AudioDevice; output_device: AudioDevice;
recordingLength: number; recording_length: number;
outputFolder: string; save_path: string;
}; };
const defaultSettings: Settings = { const defaultSettings: Settings = {
httpPort: '', http_port: 0,
inputDevice: { id: '', label: '' }, input_device: { index: 0, name: '', default_sample_rate: 0, channels: 0 },
outputDevice: { id: '', label: '' }, output_device: { index: 0, name: '', default_sample_rate: 0, channels: 0 },
recordingLength: 0, recording_length: 0,
outputFolder: '', save_path: '',
}; };
const fetchAudioDevices = async (): Promise<AudioDevice[]> => { async function fetchAudioDevices(
type: 'input' | 'output',
): Promise<AudioDevice[]> {
// Replace with actual backend call // Replace with actual backend call
// Example: return window.api.getAudioDevices(); // Example: return window.api.getAudioDevices();
return [ return apiFetch(`device/list?device_type=${type}`)
{ id: 'default-in', label: 'Default Input' }, .then((res) => res.json())
{ id: 'mic-1', label: 'Microphone 1' }, .then((data) => data.devices as AudioDevice[])
{ id: 'default-out', label: 'Default Output' }, .catch((error) => {
{ id: 'spk-1', label: 'Speakers 1' }, 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) => { const sendSettingsToBackend = (settings: Settings) => {
// Replace with actual backend call // Replace with actual backend call
@ -51,11 +70,24 @@ export default function SettingsPage() {
const navigate = useNavigate(); const navigate = useNavigate();
useEffect(() => { useEffect(() => {
fetchAudioDevices() fetchSettings()
.then((fetchedSettings) => {
console.log('Fetched settings:', fetchedSettings);
setSettings(fetchedSettings);
return null;
})
.then(() => {
return fetchAudioDevices('input');
})
.then((devices) => { .then((devices) => {
// For demo, split devices by id setInputDevices(devices);
setInputDevices(devices.filter((d) => d.id.includes('in'))); // console.log('Input devices:', devices);
setOutputDevices(devices.filter((d) => d.id.includes('out'))); return fetchAudioDevices('output');
})
.then((devices) => {
setOutputDevices(devices);
// console.log('Output devices:', devices);
return devices; return devices;
}) })
.catch((error) => { .catch((error) => {
@ -78,16 +110,16 @@ export default function SettingsPage() {
const handleFolderChange = async () => { const handleFolderChange = async () => {
// Replace with actual folder picker // Replace with actual folder picker
// Example: const folder = await window.api.selectFolder(); // Example: const folder = await window.api.selectFolder();
const folder = window.prompt( // const folder = window.prompt(
'Enter output folder path:', // 'Enter output folder path:',
settings.outputFolder, // settings.outputFolder,
); // );
if (folder !== null) { // if (folder !== null) {
setSettings((prev) => ({ // setSettings((prev) => ({
...prev, // ...prev,
outputFolder: folder, // outputFolder: folder,
})); // }));
} // }
}; };
return ( return (
@ -109,9 +141,15 @@ export default function SettingsPage() {
variant="standard" variant="standard"
type="text" type="text"
name="httpPort" name="httpPort"
value={settings.httpPort} value={settings.http_port}
onBlur={() => console.log('port blur')}
onChange={(e) => { 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]" className="ml-2 text-white w-[150px]"
/> />
@ -121,15 +159,24 @@ export default function SettingsPage() {
<Select <Select
variant="standard" variant="standard"
name="inputDevice" name="inputDevice"
value={settings.inputDevice} value={settings.input_device.index}
onChange={(e) => { 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" className="ml-2 w-64"
> >
{inputDevices.map((dev) => ( {inputDevices.map((dev) => (
<MenuItem key={dev.id} value={dev.id}> <MenuItem key={dev.index} value={dev.index}>
{dev.label} {dev.name}
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>
@ -139,18 +186,23 @@ export default function SettingsPage() {
<Select <Select
variant="standard" variant="standard"
name="outputDevice" name="outputDevice"
value={settings.outputDevice} value={settings.output_device.index}
onChange={(e) => { onChange={(e) => {
setSettings((prev) => ({ const newDevice = outputDevices.find(
...prev, (dev) => dev.index === Number(e.target.value),
outputDevice: e.target.value, );
})); if (newDevice) {
setSettings((prev) => ({
...prev,
output_device: newDevice,
}));
}
}} }}
className="ml-2 w-64" className="ml-2 w-64"
> >
{outputDevices.map((dev) => ( {outputDevices.map((dev) => (
<MenuItem key={dev.id} value={dev.id}> <MenuItem key={dev.index} value={dev.index}>
{dev.label} {dev.name}
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>
@ -161,12 +213,14 @@ export default function SettingsPage() {
variant="standard" variant="standard"
type="text" type="text"
name="recordingLength" name="recordingLength"
value={settings.recordingLength} value={settings.recording_length}
onChange={(e) => { onChange={(e) => {
setSettings((prev) => ({ if (!Number.isNaN(Number(e.target.value))) {
...prev, setSettings((prev) => ({
recordingLength: e.target.value, ...prev,
})); recording_length: Number(e.target.value),
}));
}
}} }}
className="ml-2 w-[150px]" className="ml-2 w-[150px]"
/> />
@ -177,8 +231,8 @@ export default function SettingsPage() {
<TextField <TextField
variant="standard" variant="standard"
type="text" type="text"
name="outputFolder" name="savePath"
value={settings.outputFolder} value={settings.save_path}
className="ml-2 w-[300px]" className="ml-2 w-[300px]"
/> />
<button <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 AudioTrimmer from './AudioTrimer';
import { ClipMetadata } from '../../redux/types'; import { ClipMetadata } from '../../redux/types';
import { useAppDispatch, useAppSelector } from '../hooks'; import { useAppDispatch, useAppSelector } from '../hooks';
import { apiFetch } from '../api';
export interface ClipListProps { export interface ClipListProps {
collection: string; collection: string;
@ -77,19 +78,16 @@ export default function ClipList({ collection }: ClipListProps) {
payload: { collection, newMetadata }, payload: { collection, newMetadata },
}); });
try { try {
const response = await fetch( const response = await apiFetch('meta/collection/clips/reorder', {
'http://localhost:5010/meta/collection/clips/reorder', method: 'POST',
{ headers: {
method: 'POST', 'Content-Type': 'application/json',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: collection,
clips: newMetadata.clips,
}),
}, },
); body: JSON.stringify({
name: collection,
clips: newMetadata.clips,
}),
});
const data = await response.json(); const data = await response.json();
console.log('handle reorder return:', data.collections); console.log('handle reorder return:', data.collections);
dispatch({ type: 'metadata/setAllData', payload: data }); dispatch({ type: 'metadata/setAllData', payload: data });
@ -105,7 +103,7 @@ export default function ClipList({ collection }: ClipListProps) {
type: 'metadata/deleteClip', type: 'metadata/deleteClip',
payload: { collection, clip: meta }, payload: { collection, clip: meta },
}); });
fetch('http://localhost:5010/meta/collection/clips/remove', { apiFetch('meta/collection/clips/remove', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -126,7 +124,7 @@ export default function ClipList({ collection }: ClipListProps) {
type: 'metadata/moveClip', type: 'metadata/moveClip',
payload: { sourceCollection: collection, targetCollection, clip: meta }, payload: { sourceCollection: collection, targetCollection, clip: meta },
}); });
fetch('http://localhost:5010/meta/collection/clips/move', { apiFetch('meta/collection/clips/move', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -147,19 +145,16 @@ export default function ClipList({ collection }: ClipListProps) {
type: 'metadata/editClip', type: 'metadata/editClip',
payload: { collection, clip: meta }, payload: { collection, clip: meta },
}); });
const response = await fetch( const response = await apiFetch('meta/collection/clips/edit', {
'http://localhost:5010/meta/collection/clips/edit', method: 'POST',
{ headers: {
method: 'POST', 'Content-Type': 'application/json',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: collection,
clip: meta,
}),
}, },
); body: JSON.stringify({
name: collection,
clip: meta,
}),
});
await response.json(); await response.json();
// console.log('handle clip save return:', data.collections); // console.log('handle clip save return:', data.collections);
dispatch({ dispatch({