Providing State-of-the-Art Irish Speech and Language Technologies to the Public since 2011
This is the source code for the main website of the Abair project developed by the Phonetics and Speech Laboratory at Trinity College Dublin.
The codebase was developed for two audiences:
- Public
- Developers
The public must be able to easily and reliably access the speech technologies and applications developed by the Abair group.
New developers (often students) must experience a frictionless onboarding process where they can quickly understand the codebase and become active contributors.
The design for the Abair website follows the KISS principle.
Code should be minimal, modular, with clear documentation and arranged in a logical structure.
The tools employed, e.g. React, Supabase, are widely used, well maintained and with comprehensive documentation.
This template was used to initally structure the project (not all features are used).
Abair is built using the following:
- Abair Component Library
- Built using Storybook (See github repository for more details)
Clone project from Github:
git clone https://github.com/JohnSloan8/abair.git
Install dependencies:
npm install
Run dev environment:
npm run dev
Build:
npm run build
Run a preview of the build from the dist folder in http:
npm run preview
Same with https:
npm run https-preview
Supabase is used as the back-end for this site. This includes the following open-source tools out of the box:
- Database - PostgreSQL
- API endpoints - PostgREST
- Authentication - GoTrue
- Storage - AWS
You will need to request access to the Abair project from the project manager. Once granted, create a .env file in the root of the project for environment variables:
/.env
VITE_SUPABASE_URL=https://gheiwof...
VITE_SUPABASE_ANON_KEY=fhIend9wqb...
The directories in the src folder are:
π config
π display
π error-handling
π models
π routes
π services
π store
Each page on Abair consists of π components, π controllers, and π sections. Each is explained below
These are independent, reusable pieces of code. They are hosted in a seperate npm package which is built using Storybook and can be accessed here.
- Material UI is the styling library utilised for components
- Recoil State is always passed to components as arguments.
- React useState may be utilised within the component.
- Components tend to have a lot of parameters as many argments are passed.
Example: The AbSlider component has 8 parameters, 5 of which are predefined.
const AbSlider = ({
min = 0,
max = 100,
value,
step = 1,
onChange,
icon,
iconSize = 'medium',
color = 'secondary.main',
}: AbSliderProps) => {
return (
<Stack
spacing={2}
py={{ sm: 2, xs: 0.5 }}
sx={{ width: '100%' }}
direction="row"
alignItems="center"
justifyContent="center"
>
{icon !== undefined && <SvgIcon component={icon} sx={{ color: color }} fontSize={iconSize} />}
<Slider
aria-label="Speed"
valueLabelDisplay="auto"
value={value}
min={min}
step={step}
max={max}
sx={{ color: color }}
onChange={onChange}
/>
</Stack>
);
};
Controllers interact with the state library. They access (and update) state, passing it as arguments to components if necessary.
- They never use parameters.
- May or may not return HTML.
Example: The SynthesisSpeed Controller subscribes to the Recoil useSynthesisSpeed() and useSynthesisVoiceIndex() functions to get (and set) state variables. These are then passed as arguments to the AbSlider component (described above).
/src/controllers/Synthesis/SynthesisSpeed/SynthesisSpeed.tsx
...
const SynthesisSpeed = () => {
const { synthesisSpeed, setSynthesisSpeed } = useSynthesisSpeed();
const { synthesisVoiceIndex } = useSynthesisVoiceIndex();
return synthesisVoiceIndex !== -1 ? (
<AbSlider
min={0.5}
value={synthesisSpeed}
max={1.5}
onChange={(e) => setSynthesisSpeed(parseFloat((e.target as HTMLInputElement).value))}
step={0.1}
icon={SpeedIcon}
/>
) : null;
};
A section is a block of reusable code which may contain a number of controllers and/or components, e.g. Headers and Footers.
A page may contain any number of sections, controllers, and components.
Example: The SynthesisSpeed controller is used on the abair.ie/synthesis page, alongside the SynthesisVoiceButtons, Gender, SynthesisPitch, and SynthesisModel controllers.
/src/pages/SpeechSynthesis/SpeechSynthesis.tsx
...
<Grid>
<Box minHeight={'69px'}>
<SynthesisVoiceButtons />
</Box>
<GenderChoices />
<Box width={'90%'}>
<Box minHeight={{ sm: '62px', xs: '52px' }}>
<SynthesisSpeed />
</Box>
<Box minHeight={{ sm: '62px', xs: '52px' }}>
<SynthesisPitch />
</Box>
</Box>
<Box minHeight={'77px'}>
<SynthesisModel />
</Box>
</Grid>
...
Contains type descriptions for the objects used in the project.
interface synthesisVoiceModel {
name: string;
gender: string;
locale: string;
mode?: string;
shortCode: string;
voices: string[];
pitchRange: number[];
speedRange: number[];
pitch: number;
speed: number;
}
All routes for the project are listed in the src/routes/index.ts file, with redering handled by React Router taking place in src/routes/Pages/Pages.tsx. A new route can be added by appending to the list:
const routes: Routes = {
[Pages.Home]: {
component: asyncComponentLoader(() => import('@/display/pages/Home')),
path: '/',
title: 'home',
icon: HomeIcon,
showInSidebar: true,
},
[Pages.SpeechSynthesis]: {
component: asyncComponentLoader(() => import('@/display/pages/SpeechSynthesis')),
path: '/speech-synthesis',
...
All calls to external APIs take place here. These involve requests to the Abair speech API for synthesis and recognition, and to Supabase for data stored there (e.g. profile information, publications, images, request history etc.)
All state management is managed here. Each piece of state is defined with a custom React hook exported.
/src/store/synthesis/audio.ts
...
const synthesisAudioState = atom<string>({
key: 'synthesis-audio-state',
default: '',
});
const useSynthesisAudio = () => {
const [synthesisAudio, setSynthesisAudio] = useRecoilState(synthesisAudioState);
return { synthesisAudio, setSynthesisAudio };
};
...
As much as possible, state operations should be done in the store rather than in components (selecting, filtering etc.). For example, determining whether the text box for typing a synthesis request is empty or not:
/src/store/synthesis/audio.ts
...
const isSynthesisAudioEmpty = selector({
key: 'synthesis-audio-empty-state',
get: ({ get }) => {
const data = get(synthesisAudioState);
return data.length > 0 ? false : true;
},
});
...