Files
news-reader-actions-test/services/audioExportService.ts
Tony0410 9025d1b8f1 Major feature update: Enhanced RSS, voice fixes, dark mode, and UI improvements
## 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>
2025-11-28 00:42:14 +00:00

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`);
};