feat: Update dependencies and add new voice
- Updates project dependencies to latest versions for improved performance and security. - Adds a new voice option, 'Aoede', to the available voices. - Modifies the voice change handler to force re-generation of future audio segments for the selected article. - Updates Vite configuration to align with newer Vite versions and load environment variables correctly. - Adjusts TypeScript configuration for a more modern setup. - Removes unnecessary configuration from Nginx file.
This commit is contained in:
38
App.tsx
38
App.tsx
@@ -91,6 +91,8 @@ export default function App() {
|
||||
const segmentsToBuffer = article.segments.slice(currentIndex, currentIndex + 5);
|
||||
|
||||
for (const seg of segmentsToBuffer) {
|
||||
// Only process if we don't have audio URL, and it's not currently loading.
|
||||
// This handles cases where we cleared the URL to force regeneration.
|
||||
if (!seg.audioUrl && !seg.isLoading && !seg.hasError) {
|
||||
processSegmentAudio(article.id, seg.id, seg.text, playerState.selectedVoice);
|
||||
}
|
||||
@@ -99,6 +101,38 @@ export default function App() {
|
||||
|
||||
// -- Handlers --
|
||||
|
||||
const handleVoiceChange = useCallback((newVoice: VoiceName) => {
|
||||
setPlayerState(prev => ({ ...prev, selectedVoice: newVoice }));
|
||||
|
||||
// Force flush future buffer so new voice is applied immediately
|
||||
setQueue(prevQueue => prevQueue.map(article => {
|
||||
// If this is the currently active article
|
||||
if (article.id === playerState.currentArticleId) {
|
||||
return {
|
||||
...article,
|
||||
segments: article.segments.map((seg, idx) => {
|
||||
// Keep the current segment (and past ones) to avoid cutting off mid-speech abruptly
|
||||
if (idx <= article.currentSegmentIndex) {
|
||||
return seg;
|
||||
}
|
||||
// Invalidate all future segments
|
||||
return { ...seg, audioUrl: undefined, isLoading: false, hasError: false };
|
||||
})
|
||||
};
|
||||
}
|
||||
// For inactive articles, invalidate everything
|
||||
return {
|
||||
...article,
|
||||
segments: article.segments.map(seg => ({
|
||||
...seg,
|
||||
audioUrl: undefined,
|
||||
isLoading: false,
|
||||
hasError: false
|
||||
}))
|
||||
};
|
||||
}));
|
||||
}, [playerState.currentArticleId]);
|
||||
|
||||
const handleAddUrl = async () => {
|
||||
if (!inputUrl.trim()) return;
|
||||
|
||||
@@ -373,8 +407,8 @@ export default function App() {
|
||||
<div className="flex items-center gap-4">
|
||||
<VoiceSelector
|
||||
selectedVoice={playerState.selectedVoice}
|
||||
onVoiceChange={(v) => setPlayerState(prev => ({ ...prev, selectedVoice: v }))}
|
||||
disabled={playerState.isPlaying}
|
||||
onVoiceChange={handleVoiceChange}
|
||||
// Removed disabled prop to allow switching while playing
|
||||
/>
|
||||
<button
|
||||
onClick={() => setShowSettings(!showSettings)}
|
||||
|
||||
0
Dockerfile
Normal file
0
Dockerfile
Normal file
@@ -5,8 +5,9 @@ export const AVAILABLE_VOICES = [
|
||||
{ name: VoiceName.Puck, label: 'Puck (Standard American, Male)' },
|
||||
{ name: VoiceName.Charon, label: 'Charon (Deep, Authoritative Male)' },
|
||||
{ name: VoiceName.Kore, label: 'Kore (Soft, Calm Female)' },
|
||||
{ name: VoiceName.Fenrir, label: 'Fenrir (Energetic, Mid-Atlantic Male)' },
|
||||
{ name: VoiceName.Fenrir, label: 'Fenrir (Mid-Atlantic/British Style, Male)' },
|
||||
{ name: VoiceName.Zephyr, label: 'Zephyr (Clear, Professional Female)' },
|
||||
{ name: VoiceName.Aoede, label: 'Aoede (Confident, Professional Female)' },
|
||||
];
|
||||
|
||||
export const MIN_SPEED = 0.5;
|
||||
|
||||
@@ -20,15 +20,17 @@
|
||||
border-radius: 3px;
|
||||
}
|
||||
</style>
|
||||
<script type="importmap">
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"react-dom/": "https://aistudiocdn.com/react-dom@^19.2.0/",
|
||||
"lucide-react": "https://aistudiocdn.com/lucide-react@^0.554.0",
|
||||
"@google/genai": "https://aistudiocdn.com/@google/genai@^1.30.0",
|
||||
"react": "https://aistudiocdn.com/react@^19.2.0",
|
||||
"react/": "https://aistudiocdn.com/react@^19.2.0/",
|
||||
"react-dom/": "https://aistudiocdn.com/react-dom@^19.2.0/",
|
||||
"uuid": "https://aistudiocdn.com/uuid@^13.0.0"
|
||||
"uuid": "https://aistudiocdn.com/uuid@^13.0.0",
|
||||
"@vitejs/plugin-react": "https://aistudiocdn.com/@vitejs/plugin-react@^5.1.1",
|
||||
"vite": "https://aistudiocdn.com/vite@^7.2.2"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
1
nginx.conf
Normal file
1
nginx.conf
Normal file
@@ -0,0 +1 @@
|
||||
<EFBFBD><EFBFBD><EFBFBD>z
|
||||
15
package.json
15
package.json
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"name": "newscaster-ai",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -9,16 +8,18 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"lucide-react": "^0.554.0",
|
||||
"@google/genai": "^1.30.0",
|
||||
"lucide-react": "^0.554.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.14.0",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"typescript": "~5.8.2",
|
||||
"vite": "^6.2.0"
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^6.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"moduleResolution": "bundler",
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"allowJs": true,
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true
|
||||
}
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
1
types.ts
1
types.ts
@@ -5,6 +5,7 @@ export enum VoiceName {
|
||||
Kore = 'Kore',
|
||||
Fenrir = 'Fenrir',
|
||||
Zephyr = 'Zephyr',
|
||||
Aoede = 'Aoede',
|
||||
}
|
||||
|
||||
export enum PlaybackStatus {
|
||||
|
||||
@@ -1,23 +1,16 @@
|
||||
import path from 'path';
|
||||
import { defineConfig, loadEnv } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, '.', '');
|
||||
// Load env file based on `mode` in the current working directory.
|
||||
const env = loadEnv(mode, (process as any).cwd(), '');
|
||||
|
||||
return {
|
||||
server: {
|
||||
port: 3000,
|
||||
host: '0.0.0.0',
|
||||
},
|
||||
plugins: [react()],
|
||||
define: {
|
||||
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
|
||||
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, '.'),
|
||||
}
|
||||
// This allows the app code to continue using process.env.API_KEY
|
||||
// even though it is running in the browser.
|
||||
'process.env.API_KEY': JSON.stringify(env.API_KEY)
|
||||
}
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user