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:
Anthony
2025-11-19 20:46:02 +08:00
parent 0b10d71554
commit 43435166f8
9 changed files with 80 additions and 55 deletions

38
App.tsx
View File

@@ -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
View File

View 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;

View File

@@ -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
View File

@@ -0,0 +1 @@
<EFBFBD><EFBFBD><EFBFBD>z

View File

@@ -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"
}
}
}

View File

@@ -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"]
}

View File

@@ -5,6 +5,7 @@ export enum VoiceName {
Kore = 'Kore',
Fenrir = 'Fenrir',
Zephyr = 'Zephyr',
Aoede = 'Aoede',
}
export enum PlaybackStatus {

View File

@@ -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, '.', '');
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, '.'),
}
}
};
});
// Load env file based on `mode` in the current working directory.
const env = loadEnv(mode, (process as any).cwd(), '');
return {
plugins: [react()],
define: {
// 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)
}
};
});