mirror of
https://github.com/Tony0410/News-reader-pro.git
synced 2026-05-24 13:21:40 +08:00
## New Features - Article Summary Mode: AI-generated 30-second summaries with complexity analysis - Reading Stats Dashboard: Track articles read, listening time, and streaks - Bookmark/Resume: Auto-save progress when pausing, resume from where you left off - Audio Export: Export articles as downloadable WAV files - RSS Feed Manager: Subscribe to feeds with real-time validation and 31+ recommendations - Smart Speed: Auto-adjust playback based on article complexity - Voice Moods: Quick presets for different listening scenarios ## RSS Enhancements - Expanded recommendations from 8 to 31 sources across 5 categories: * General News (9 sources) * Technology (8 sources) * Business & Finance (5 sources) * Science & Research (5 sources) * International News (4 sources) - Real-time URL validation with visual feedback - Detailed error messages for different failure scenarios - Always-visible categorized recommendations - Auto-loading articles when feeds are added ## Bug Fixes - Fixed voice selection: Selected voice now consistently applies to playback - Implemented voice generation counter to prevent voice mixing between paragraphs - Fixed speed control to snap to clean 0.5 increments (1.0, 1.5, 2.0, etc.) - Fixed dark mode toggle by configuring Tailwind CDN for class-based dark mode - Removed vibe visualizer animation ## UI/UX Improvements - Redesigned voice selector with prominent voice panel and preview functionality - Added voice cards with emojis and descriptions - Enhanced feature toolbar with quick access to all new features - Improved reader view with better typography and reading modes - Added ambient reading modes (clean, sepia, night light) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
136 lines
3.9 KiB
TypeScript
136 lines
3.9 KiB
TypeScript
import { Article, AudioSegment } from '../types';
|
|
import { SAMPLE_RATE } from '../constants';
|
|
|
|
/**
|
|
* Combines multiple audio segments into a single downloadable file.
|
|
* Since browser-based MP3 encoding is complex, we export as WAV format
|
|
* which is widely supported and maintains quality.
|
|
*/
|
|
export const exportArticleAudio = async (
|
|
article: Article,
|
|
onProgress?: (progress: number) => void
|
|
): Promise<Blob> => {
|
|
const audioSegments = article.segments.filter(seg => seg.audioUrl);
|
|
|
|
if (audioSegments.length === 0) {
|
|
throw new Error('No audio segments available for export');
|
|
}
|
|
|
|
// Fetch all audio data
|
|
const audioBuffers: ArrayBuffer[] = [];
|
|
let totalLength = 0;
|
|
|
|
for (let i = 0; i < audioSegments.length; i++) {
|
|
const segment = audioSegments[i];
|
|
if (!segment.audioUrl) continue;
|
|
|
|
try {
|
|
const response = await fetch(segment.audioUrl);
|
|
const arrayBuffer = await response.arrayBuffer();
|
|
audioBuffers.push(arrayBuffer);
|
|
|
|
// Parse WAV to get PCM data length (skip 44-byte header)
|
|
totalLength += arrayBuffer.byteLength - 44;
|
|
|
|
if (onProgress) {
|
|
onProgress(Math.round(((i + 1) / audioSegments.length) * 100));
|
|
}
|
|
} catch (e) {
|
|
console.warn(`Failed to fetch segment ${i}:`, e);
|
|
}
|
|
}
|
|
|
|
if (audioBuffers.length === 0) {
|
|
throw new Error('Failed to fetch any audio segments');
|
|
}
|
|
|
|
// Combine PCM data from all WAV files
|
|
const combinedPcm = new Uint8Array(totalLength);
|
|
let offset = 0;
|
|
|
|
for (const buffer of audioBuffers) {
|
|
// Skip WAV header (44 bytes) and copy PCM data
|
|
const pcmData = new Uint8Array(buffer, 44);
|
|
combinedPcm.set(pcmData, offset);
|
|
offset += pcmData.length;
|
|
}
|
|
|
|
// Create new WAV file with combined data
|
|
return createWavFile(combinedPcm, SAMPLE_RATE);
|
|
};
|
|
|
|
/**
|
|
* Creates a WAV file from PCM data
|
|
*/
|
|
const createWavFile = (pcmData: Uint8Array, sampleRate: number): Blob => {
|
|
const numChannels = 1;
|
|
const bitsPerSample = 16;
|
|
const byteRate = sampleRate * numChannels * (bitsPerSample / 8);
|
|
const blockAlign = numChannels * (bitsPerSample / 8);
|
|
const dataSize = pcmData.length;
|
|
const fileSize = 44 + dataSize;
|
|
|
|
const buffer = new ArrayBuffer(fileSize);
|
|
const view = new DataView(buffer);
|
|
|
|
// RIFF header
|
|
writeString(view, 0, 'RIFF');
|
|
view.setUint32(4, fileSize - 8, true);
|
|
writeString(view, 8, 'WAVE');
|
|
|
|
// fmt chunk
|
|
writeString(view, 12, 'fmt ');
|
|
view.setUint32(16, 16, true); // chunk size
|
|
view.setUint16(20, 1, true); // PCM format
|
|
view.setUint16(22, numChannels, true);
|
|
view.setUint32(24, sampleRate, true);
|
|
view.setUint32(28, byteRate, true);
|
|
view.setUint16(32, blockAlign, true);
|
|
view.setUint16(34, bitsPerSample, true);
|
|
|
|
// data chunk
|
|
writeString(view, 36, 'data');
|
|
view.setUint32(40, dataSize, true);
|
|
|
|
// PCM data
|
|
const outputArray = new Uint8Array(buffer);
|
|
outputArray.set(pcmData, 44);
|
|
|
|
return new Blob([buffer], { type: 'audio/wav' });
|
|
};
|
|
|
|
const writeString = (view: DataView, offset: number, string: string) => {
|
|
for (let i = 0; i < string.length; i++) {
|
|
view.setUint8(offset + i, string.charCodeAt(i));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Triggers download of the audio file
|
|
*/
|
|
export const downloadAudio = (blob: Blob, filename: string): void => {
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = filename.endsWith('.wav') ? filename : `${filename}.wav`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
};
|
|
|
|
/**
|
|
* Export and download article audio with progress tracking
|
|
*/
|
|
export const exportAndDownloadArticle = async (
|
|
article: Article,
|
|
onProgress?: (progress: number) => void
|
|
): Promise<void> => {
|
|
const blob = await exportArticleAudio(article, onProgress);
|
|
const safeTitle = article.title
|
|
.replace(/[^a-zA-Z0-9\s]/g, '')
|
|
.replace(/\s+/g, '_')
|
|
.substring(0, 50);
|
|
downloadAudio(blob, `${safeTitle}_newscaster.wav`);
|
|
};
|