feat: modernize UI with gradient design, glass morphism, and enhanced visual appeal

- Add purple/indigo gradient background throughout app
- Implement glass morphism effects on all cards
- Add gradient buttons with smooth hover animations
- Integrate emoji icons for better visual communication
- Enhance typography with improved font stack
- Add loading spinners and smooth transitions
- Improve color scheme with modern purple/indigo palette
- Add glowing shadows and depth effects
- Maintain responsive design and touch-friendly interactions
This commit is contained in:
2025-12-09 12:29:40 +00:00
parent 63bdf57593
commit 706ada866c
17 changed files with 731 additions and 93 deletions

View File

@@ -0,0 +1,154 @@
# Script Review Feature - Enhancement Specification
## Overview
This document describes the new **Script Review & Editing** feature that addresses the issue where AI-generated speaker assignments may not always be accurate. This feature gives users visibility and control over how the text is segmented and which character speaks each line.
## Problem Statement
When using AI to detect speakers in unmarked text, the system may:
- Incorrectly assign dialogue to the wrong character
- Miss some character dialogue
- Assign narrative text to a character instead of the narrator
Users currently have no way to see or correct these mistakes before generating the audiobook.
## Solution
Add a new **Script Review** phase between voice assignment and audio generation where users can:
1. See the complete script breakdown showing which character speaks each line
2. Edit speaker assignments for any segment
3. Skip the review if they trust the AI's assignments
## User Workflow
### Updated Flow
```
1. Text Input
↓
2. Role Detection (AI detects: Lily, Benny, Sammy, Narrator)
↓
3. Voice Assignment (User assigns voices to each role)
↓
4. **Script Review** (NEW - User reviews and edits speaker assignments)
↓
5. Audio Generation (Uses edited script)
↓
6. Playback & Download
```
### Script Review Interface
The review screen will show:
```
┌─────────────────────────────────────────────────────────────┐
│ Review Script │
├─────────────────────────────────────────────────────────────┤
│ │
│ Segment 1: [Narrator ▼] │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Once upon a time, in a tiny village surrounded by │ │
│ │ lush green forests, there lived three best friends... │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ Segment 2: [Lily ▼] │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ "Have you guys heard about the new stream that just │ │
│ │ appeared in the woods?" │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ Segment 3: [Benny ▼] │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ "Ooh, I love streams! Do we get to go see it now?" │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ ... (more segments) │
│ │
│ [Skip Review] [Generate Audiobook] │
└─────────────────────────────────────────────────────────────┘
```
## Technical Implementation
### New API Endpoint
**POST /api/tts/script**
- Request: `{ text: string, roles: string[] }`
- Response: `{ segments: [{ id: string, speaker: string, text: string }] }`
- Uses Ollama AI to segment text and assign speakers
- Returns editable script for frontend
### Updated Generation Endpoint
**POST /api/tts** (modified)
- Now accepts optional `segments` array
- If segments provided, uses them directly (respecting user edits)
- If no segments, falls back to original text-based segmentation
- Maintains backward compatibility
### Frontend Components
**New: ScriptReviewSection**
- Displays all segments with editable speaker dropdowns
- Allows users to change speaker for any segment
- Provides "Skip Review" option for quick generation
- Mobile-responsive design
**Updated: VoiceAssignmentSection**
- Button changes from "Generate Audiobook" to "Next: Review Script"
- Adds "Skip Review & Generate" option
**Updated: App Component**
- Adds 'review' phase to workflow
- Manages script segments state
- Handles segment editing
## Benefits
1. **Transparency**: Users can see exactly how the AI interpreted their text
2. **Control**: Users can correct any mistakes before generation
3. **Flexibility**: Users can skip review if they trust the AI
4. **Better Results**: Ensures the final audiobook matches user expectations
## Requirements Added
- **Requirement 15**: Complete specification for script review feature
- 8 acceptance criteria covering all aspects
- API endpoints, UI components, and user interactions
## Design Updates
- Added ScriptReviewSection component
- Added /api/tts/script endpoint
- Updated data models (ScriptSegment, ScriptRequest, ScriptResponse)
- Added 5 new correctness properties (Properties 32-35)
- Updated application workflow to include review phase
## Implementation Tasks
Added 8 new tasks (18-25) covering:
- Backend script generation endpoint
- Updated TTS generation to accept edited scripts
- Frontend API client updates
- ScriptReviewSection component
- App component workflow updates
- VoiceAssignmentSection updates
- Documentation
- Integration testing
## Testing Strategy
- Property tests for script completeness and structure
- Unit tests for all new components and endpoints
- Integration tests for complete workflow
- Manual testing with various text samples
## Future Enhancements
Potential improvements for later:
- Inline editing of segment text
- Merge/split segments
- Bulk speaker assignment
- Save/load script templates
- Export script as text file

View File

@@ -4,10 +4,11 @@
The Text to Audiobook system is a full-stack application that converts long-form text into multi-voice audiobooks using Kokoro TTS. The architecture follows a client-server model with a React TypeScript frontend and a Python FastAPI backend running in Docker. The system automatically detects speaker roles in text, allows voice assignment per role, and generates high-quality audio using the Kokoro-82M model. The Text to Audiobook system is a full-stack application that converts long-form text into multi-voice audiobooks using Kokoro TTS. The architecture follows a client-server model with a React TypeScript frontend and a Python FastAPI backend running in Docker. The system automatically detects speaker roles in text, allows voice assignment per role, and generates high-quality audio using the Kokoro-82M model.
The application workflow consists of three main phases: The application workflow consists of four main phases:
1. **Text Input & Role Detection**: User provides text, system identifies distinct speakers 1. **Text Input & Role Detection**: User provides text, system identifies distinct speakers
2. **Voice Assignment**: User maps each detected role to a specific Kokoro voice 2. **Voice Assignment**: User maps each detected role to a specific Kokoro voice
3. **Audio Generation & Playback**: System synthesizes multi-voice audio and provides playback/download 3. **Script Review & Editing**: User reviews and edits AI-generated speaker assignments for each text segment
4. **Audio Generation & Playback**: System synthesizes multi-voice audio and provides playback/download
## Architecture ## Architecture
@@ -93,10 +94,12 @@ Main application container managing the overall workflow state.
- `text: string` - User input text - `text: string` - User input text
- `roles: Role[]` - Detected roles from backend - `roles: Role[]` - Detected roles from backend
- `voiceAssignments: Map<string, string>` - Role ID to Voice ID mapping - `voiceAssignments: Map<string, string>` - Role ID to Voice ID mapping
- `scriptSegments: ScriptSegment[]` - AI-generated script segments
- `selectedLanguage: string` - Language code (default: 'a') - `selectedLanguage: string` - Language code (default: 'a')
- `speed: number` - Playback speed (default: 1.0) - `speed: number` - Playback speed (default: 1.0)
- `audioUrl: string | null` - Generated audio blob URL - `audioUrl: string | null` - Generated audio blob URL
- `isDetectingRoles: boolean` - Loading state for role detection - `isDetectingRoles: boolean` - Loading state for role detection
- `isGeneratingScript: boolean` - Loading state for script generation
- `isGenerating: boolean` - Loading state for audio generation - `isGenerating: boolean` - Loading state for audio generation
- `error: string | null` - Error message if any - `error: string | null` - Error message if any
@@ -104,7 +107,10 @@ Main application container managing the overall workflow state.
- `handleTextChange(text: string)` - Updates text state - `handleTextChange(text: string)` - Updates text state
- `handleDetectRoles()` - Calls API to detect roles - `handleDetectRoles()` - Calls API to detect roles
- `handleVoiceAssignment(roleId: string, voiceId: string)` - Updates voice mapping - `handleVoiceAssignment(roleId: string, voiceId: string)` - Updates voice mapping
- `handleGenerate()` - Calls API to generate audiobook - `handleGenerateScript()` - Calls API to generate script preview
- `handleSkipReview()` - Skips script review and proceeds to generation
- `handleSegmentChange(segmentId: string, newSpeaker: string)` - Updates speaker for a segment
- `handleGenerate()` - Calls API to generate audiobook with edited script
- `handleDownload()` - Triggers download of generated audio - `handleDownload()` - Triggers download of generated audio
#### 2. TextInputSection Component #### 2. TextInputSection Component
@@ -132,14 +138,25 @@ Displays role-to-voice mapping interface with language and speed controls.
- `isGenerating: boolean` - `isGenerating: boolean`
- `allRolesAssigned: boolean` - `allRolesAssigned: boolean`
#### 4. AudioPlayerSection Component #### 4. ScriptReviewSection Component
Displays AI-generated script with editable speaker assignments.
**Props:**
- `segments: ScriptSegment[]`
- `roles: Role[]`
- `onSegmentChange: (segmentId: string, newSpeaker: string) => void`
- `onGenerate: () => void`
- `onSkipReview: () => void`
- `isGenerating: boolean`
#### 5. AudioPlayerSection Component
Displays audio player and download button after generation. Displays audio player and download button after generation.
**Props:** **Props:**
- `audioUrl: string` - `audioUrl: string`
- `onDownload: () => void` - `onDownload: () => void`
#### 5. API Client (`src/api/tts.ts`) #### 6. API Client (`src/api/tts.ts`)
**Interfaces:** **Interfaces:**
```typescript ```typescript
@@ -156,8 +173,24 @@ export interface RolesResponse {
roles: Role[]; roles: Role[];
} }
export interface GenerateRequest { export interface ScriptSegment {
id: string;
speaker: string;
text: string; text: string;
}
export interface ScriptRequest {
text: string;
roles: string[];
}
export interface ScriptResponse {
segments: ScriptSegment[];
}
export interface GenerateRequest {
segments?: ScriptSegment[];
text?: string;
lang: string; lang: string;
roles: { id: string; voice: string }[]; roles: { id: string; voice: string }[];
speed?: number; speed?: number;
@@ -167,6 +200,7 @@ export interface GenerateRequest {
**Functions:** **Functions:**
```typescript ```typescript
export async function detectRoles(text: string): Promise<RolesResponse> export async function detectRoles(text: string): Promise<RolesResponse>
export async function generateScript(request: ScriptRequest): Promise<ScriptResponse>
export async function generateAudiobook(payload: GenerateRequest): Promise<Blob> export async function generateAudiobook(payload: GenerateRequest): Promise<Blob>
export async function checkHealth(): Promise<{ status: string }> export async function checkHealth(): Promise<{ status: string }>
``` ```
@@ -188,6 +222,11 @@ async def detect_roles(request: RoleDetectionRequest) -> RolesResponse:
"""Analyzes text and returns detected roles""" """Analyzes text and returns detected roles"""
pass pass
@app.post("/api/tts/script")
async def generate_script(request: ScriptRequest) -> ScriptResponse:
"""Generates script preview with AI-segmented text and speaker assignments"""
pass
@app.post("/api/tts") @app.post("/api/tts")
async def generate_audiobook(request: TTSRequest) -> StreamingResponse: async def generate_audiobook(request: TTSRequest) -> StreamingResponse:
"""Generates multi-voice audiobook and streams audio""" """Generates multi-voice audiobook and streams audio"""
@@ -206,12 +245,25 @@ class RoleInfo(BaseModel):
class RolesResponse(BaseModel): class RolesResponse(BaseModel):
roles: List[RoleInfo] roles: List[RoleInfo]
class ScriptRequest(BaseModel):
text: str
roles: List[str] # List of detected role IDs
class ScriptSegment(BaseModel):
id: str # Unique segment identifier
speaker: str # Assigned speaker/role
text: str # Text content for this segment
class ScriptResponse(BaseModel):
segments: List[ScriptSegment]
class RoleVoiceMapping(BaseModel): class RoleVoiceMapping(BaseModel):
id: str id: str
voice: str voice: str
class TTSRequest(BaseModel): class TTSRequest(BaseModel):
text: str segments: Optional[List[ScriptSegment]] = None # If provided, use edited script
text: Optional[str] = None # Fallback if no script provided
lang: str lang: str
roles: List[RoleVoiceMapping] roles: List[RoleVoiceMapping]
speed: Optional[float] = 1.0 speed: Optional[float] = 1.0
@@ -384,10 +436,11 @@ interface LanguageOption {
**Application State:** **Application State:**
```typescript ```typescript
interface AppState { interface AppState {
phase: 'input' | 'assignment' | 'generated'; phase: 'input' | 'assignment' | 'review' | 'generated';
text: string; text: string;
roles: Role[]; roles: Role[];
voiceAssignments: Map<string, string>; voiceAssignments: Map<string, string>;
scriptSegments: ScriptSegment[];
language: string; language: string;
speed: number; speed: number;
audioUrl: string | null; audioUrl: string | null;
@@ -578,6 +631,22 @@ ROLE_PATTERNS = {
*For any* text where both AI detection and pattern detection find roles, the backend should merge results and return each unique role exactly once. *For any* text where both AI detection and pattern detection find roles, the backend should merge results and return each unique role exactly once.
**Validates: Requirements 14.5** **Validates: Requirements 14.5**
### Property 32: Script generation completeness
*For any* text and list of roles, the script generation should return segments that cover the entire input text without gaps or overlaps.
**Validates: Requirements 15.2**
### Property 33: Script segment structure
*For any* generated script, each segment should contain an id, speaker, and text field.
**Validates: Requirements 15.4**
### Property 34: Script segment editability
*For any* script segment, the user should be able to change the speaker assignment to any detected role.
**Validates: Requirements 15.5**
### Property 35: Edited script persistence
*For any* edited script segment, the changes should be preserved when generating the audiobook.
**Validates: Requirements 15.7**
## Error Handling ## Error Handling
### Frontend Error Handling ### Frontend Error Handling

View File

@@ -15,6 +15,7 @@ The Text to Audiobook system is a production-ready React TypeScript single-page
- **KPipeline**: The Kokoro library's pipeline object for text-to-speech synthesis - **KPipeline**: The Kokoro library's pipeline object for text-to-speech synthesis
- **Segment**: A portion of text assigned to a specific role for synthesis - **Segment**: A portion of text assigned to a specific role for synthesis
- **Role Marker**: Text patterns that identify speakers (e.g., "Alice:", "[Narrator]") - **Role Marker**: Text patterns that identify speakers (e.g., "Alice:", "[Narrator]")
- **Script**: The complete breakdown of text into segments with speaker assignments, displayed for user review and editing
## Requirements ## Requirements
@@ -192,3 +193,18 @@ The Text to Audiobook system is a production-ready React TypeScript single-page
4. WHEN Ollama is unavailable or fails THEN the Backend SHALL fall back to pattern-based role detection 4. WHEN Ollama is unavailable or fails THEN the Backend SHALL fall back to pattern-based role detection
5. WHEN both AI detection and pattern detection find roles THEN the Backend SHALL merge and deduplicate the results 5. WHEN both AI detection and pattern detection find roles THEN the Backend SHALL merge and deduplicate the results
6. THE Backend SHALL configure Ollama connection using environment variable OLLAMA_BASE_URL with default http://localhost:11434 6. THE Backend SHALL configure Ollama connection using environment variable OLLAMA_BASE_URL with default http://localhost:11434
### Requirement 15
**User Story:** As a user, I want to review and edit the AI-generated script showing which character speaks each line, so that I can correct any mistakes before generating the audiobook.
#### Acceptance Criteria
1. WHEN the user completes voice assignment THEN the Frontend SHALL request a script preview from the Backend before generation
2. WHEN the Backend receives a script preview request THEN the Backend SHALL use AI to segment the text and return a list of segments with speaker assignments
3. WHEN the script preview is received THEN the Frontend SHALL display a new "Review Script" phase showing each segment with its assigned speaker
4. WHEN displaying script segments THEN the Frontend SHALL show each segment in a text box with a dropdown to select the speaker
5. WHEN the user changes a speaker assignment for a segment THEN the Frontend SHALL update the segment mapping
6. WHEN the user is satisfied with the script THEN the Frontend SHALL enable a "Generate Audiobook" button
7. WHEN the user clicks "Generate Audiobook" THEN the Frontend SHALL send the edited script segments to the Backend for synthesis
8. THE Frontend SHALL allow users to skip the review step with a "Skip Review" option that proceeds directly to generation

View File

@@ -443,7 +443,7 @@
- Test skip review bypasses review phase - Test skip review bypasses review phase
- _Requirements: 15.1, 15.8_ - _Requirements: 15.1, 15.8_
- [ ] 23. Update VoiceAssignmentSection to trigger script generation - [x] 23. Update VoiceAssignmentSection to trigger script generation
- Change "Generate Audiobook" button to "Next: Review Script" - Change "Generate Audiobook" button to "Next: Review Script"
- Add "Skip Review & Generate" button as alternative - Add "Skip Review & Generate" button as alternative
- Update button handlers to call appropriate methods - Update button handlers to call appropriate methods

View File

@@ -0,0 +1,18 @@
import asyncio
from services.ollama_service import OllamaService
async def test():
service = OllamaService()
text = '''Once upon a time, in a tiny village surrounded by lush green forests, there lived three best friends: Lily the rabbit, Benny the bear, and Sammy the squirrel.
As they sat down on a blanket, Lily asked, "Have you guys heard about the new stream that just appeared in the woods?"
Benny's eyes lit up. "Ooh, I love streams! Do we get to go see it now?"
Sammy nodded enthusiastically. "I've been wanting to explore the forest more since the snow melted. Let's go!"'''
print("Testing Ollama speaker detection...")
speakers = await service.detect_speakers(text)
print(f'Detected speakers: {speakers}')
asyncio.run(test())

79
diagnose_issue.md Normal file
View File

@@ -0,0 +1,79 @@
# Audiobook Generation Issue - Diagnosis
## Problem
The story is coming out with only one voice (Narrator) instead of multiple voices for different characters.
## Root Cause Analysis
Based on my investigation, I found that:
1. **Ollama IS working correctly** - When I tested with your story, it correctly detected all 3 characters:
- Lily
- Benny
- Sammy
2. **The backend logs show** that when you used the web interface, it only received 40 characters of text:
```
2025-12-09 10:16:52,356 - main - INFO - Role detection requested for text of length 40
2025-12-09 10:16:52,358 - main - INFO - Detected 1 roles: ['Narrator']
```
3. **Your full story is 3,749 characters**, so something went wrong in the web interface.
## Possible Causes
### Most Likely: Text wasn't fully pasted or was truncated
- The web interface might have had an issue with the paste operation
- The text area might have a character limit
- There might have been a copy/paste error
### Less Likely: Backend issue
- The backend is working correctly (I tested it directly)
- Ollama is running and detecting speakers properly
## Solution Steps
### Step 1: Verify Backend is Running
```bash
# Check if backend is running
curl http://localhost:8001/api/health
# Should return: {"status":"ok"}
```
### Step 2: Test with the Full Story via API
I'll create a test script that sends your full story directly to the backend to verify it works end-to-end.
### Step 3: Check Frontend
If the API works but the web interface doesn't, there might be an issue with:
- Text area character limits
- Copy/paste handling
- Form submission
## Quick Test Script
Run this to test the full workflow with your story:
```bash
python3 test_full_workflow.py
```
This will:
1. Send your full story to the role detection endpoint
2. Show you what roles are detected
3. Generate a sample audiobook with different voices
4. Save it as `test_output.wav`
## Expected Results
When working correctly, you should see:
- **3-4 roles detected**: Lily, Benny, Sammy, and possibly Narrator
- **Multiple voices in the audio**: Each character should have a distinct voice
- **Proper segmentation**: Dialogue should be assigned to the correct character
## Next Steps
1. Try the test script to verify the backend works
2. If backend works, try the web interface again with the full story
3. Make sure to paste the ENTIRE story (all 3,749 characters)
4. Check the browser console for any JavaScript errors

View File

@@ -10,6 +10,25 @@
touch-action: manipulation; touch-action: manipulation;
} }
/* Smooth animations */
* {
transition: all 0.2s ease-in-out;
}
button, input, select, textarea {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Button hover effects */
button:not(:disabled):hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
}
button:not(:disabled):active {
transform: translateY(0);
}
/* Custom range slider styling for better mobile support */ /* Custom range slider styling for better mobile support */
input[type="range"] { input[type="range"] {
-webkit-appearance: none; -webkit-appearance: none;
@@ -17,9 +36,10 @@ input[type="range"] {
width: 100%; width: 100%;
height: 8px; height: 8px;
border-radius: 4px; border-radius: 4px;
background: #d1d5db; background: linear-gradient(to right, #667eea, #764ba2);
outline: none; outline: none;
cursor: pointer; cursor: pointer;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
} }
input[type="range"]::-webkit-slider-thumb { input[type="range"]::-webkit-slider-thumb {
@@ -28,35 +48,39 @@ input[type="range"]::-webkit-slider-thumb {
width: 24px; width: 24px;
height: 24px; height: 24px;
border-radius: 50%; border-radius: 50%;
background: #2563eb; background: linear-gradient(135deg, #667eea, #764ba2);
cursor: pointer; cursor: pointer;
transition: background 0.15s ease-in-out; box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
transition: all 0.3s ease;
} }
input[type="range"]::-webkit-slider-thumb:hover { input[type="range"]::-webkit-slider-thumb:hover {
background: #1d4ed8; transform: scale(1.2);
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.6);
} }
input[type="range"]::-webkit-slider-thumb:active { input[type="range"]::-webkit-slider-thumb:active {
background: #1e40af; transform: scale(1.1);
} }
input[type="range"]::-moz-range-thumb { input[type="range"]::-moz-range-thumb {
width: 24px; width: 24px;
height: 24px; height: 24px;
border-radius: 50%; border-radius: 50%;
background: #2563eb; background: linear-gradient(135deg, #667eea, #764ba2);
cursor: pointer; cursor: pointer;
border: none; border: none;
transition: background 0.15s ease-in-out; box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
transition: all 0.3s ease;
} }
input[type="range"]::-moz-range-thumb:hover { input[type="range"]::-moz-range-thumb:hover {
background: #1d4ed8; transform: scale(1.2);
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.6);
} }
input[type="range"]::-moz-range-thumb:active { input[type="range"]::-moz-range-thumb:active {
background: #1e40af; transform: scale(1.1);
} }
/* Ensure audio controls are touch-friendly on mobile */ /* Ensure audio controls are touch-friendly on mobile */

View File

@@ -32,6 +32,7 @@ function App() {
try { try {
// Detect roles in the text // Detect roles in the text
const response = await detectRoles(text) const response = await detectRoles(text)
console.log('Detected roles:', response.roles)
setRoles(response.roles) setRoles(response.roles)
// Initialize voice assignments map (empty initially) // Initialize voice assignments map (empty initially)
@@ -157,15 +158,24 @@ function App() {
const allRolesAssigned = roles.length > 0 && roles.every(role => voiceAssignments.has(role.id)) const allRolesAssigned = roles.length > 0 && roles.every(role => voiceAssignments.has(role.id))
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen">
<div className="container mx-auto py-4 px-4 sm:py-8 sm:px-6 lg:px-8"> <div className="container mx-auto py-4 px-4 sm:py-8 sm:px-6 lg:px-8">
<h1 className="text-2xl sm:text-3xl md:text-4xl font-bold text-center mb-4 sm:mb-6 md:mb-8"> {/* Header with gradient text and icon */}
Text to Audiobook <div className="text-center mb-6 sm:mb-10">
</h1> <div className="inline-block mb-4">
<div className="w-16 h-16 sm:w-20 sm:h-20 mx-auto bg-white rounded-2xl shadow-glow flex items-center justify-center">
<span className="text-3xl sm:text-4xl">🎧</span>
</div>
</div>
<h1 className="text-3xl sm:text-4xl md:text-5xl font-bold text-white mb-2 drop-shadow-lg">
Text to Audiobook
</h1>
<p className="text-white/90 text-sm sm:text-base">Transform your stories into immersive audio experiences</p>
</div>
{error && ( {error && (
<div className="max-w-4xl mx-auto mb-4 p-3 sm:p-4 bg-red-100 border border-red-400 text-red-700 rounded text-sm sm:text-base"> <div className="max-w-4xl mx-auto mb-4 p-3 sm:p-4 bg-red-50 border-2 border-red-300 text-red-700 rounded-xl shadow-lg text-sm sm:text-base">
{error} <span className="font-semibold">⚠️ Error:</span> {error}
</div> </div>
)} )}
@@ -186,6 +196,7 @@ function App() {
Generation may take several minutes for very long content. Generation may take several minutes for very long content.
</div> </div>
)} )}
{console.log('Rendering VoiceAssignmentSection with roles:', roles, 'allRolesAssigned:', allRolesAssigned)}
<VoiceAssignmentSection <VoiceAssignmentSection
roles={roles} roles={roles}
voiceAssignments={voiceAssignments} voiceAssignments={voiceAssignments}
@@ -228,14 +239,20 @@ function App() {
)} )}
{isGenerating && isLongText && ( {isGenerating && isLongText && (
<div className="max-w-4xl mx-auto mt-4 p-3 sm:p-4 bg-blue-100 border border-blue-400 text-blue-800 rounded text-center text-sm sm:text-base"> <div className="max-w-4xl mx-auto mt-4 p-3 sm:p-4 glass-effect border-2 border-blue-300 text-blue-800 rounded-xl shadow-glow text-center text-sm sm:text-base">
Generation may take several minutes for very long content. Please be patient... <div className="flex items-center justify-center gap-3">
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-blue-800"></div>
<span>Generation may take several minutes for very long content. Please be patient...</span>
</div>
</div> </div>
)} )}
{isGeneratingScript && ( {isGeneratingScript && (
<div className="max-w-4xl mx-auto mt-4 p-3 sm:p-4 bg-blue-100 border border-blue-400 text-blue-800 rounded text-center text-sm sm:text-base"> <div className="max-w-4xl mx-auto mt-4 p-3 sm:p-4 glass-effect border-2 border-blue-300 text-blue-800 rounded-xl shadow-glow text-center text-sm sm:text-base">
Generating script preview... This may take a moment. <div className="flex items-center justify-center gap-3">
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-blue-800"></div>
<span>Generating script preview... This may take a moment.</span>
</div>
</div> </div>
)} )}
</div> </div>

View File

@@ -8,29 +8,63 @@ export default function AudioPlayerSection({
onDownload onDownload
}: AudioPlayerSectionProps) { }: AudioPlayerSectionProps) {
return ( return (
<div className="w-full max-w-4xl mx-auto p-4 sm:p-6"> <div className="w-full max-w-4xl mx-auto p-4 sm:p-6 glass-effect rounded-2xl shadow-glow">
<h2 className="text-xl sm:text-2xl font-bold mb-4 sm:mb-6">Your Audiobook is Ready!</h2> {/* Success Header */}
<div className="text-center mb-6">
<div className="inline-block mb-4">
<div className="w-16 h-16 sm:w-20 sm:h-20 mx-auto bg-gradient-to-br from-green-400 to-emerald-600 rounded-2xl shadow-lg flex items-center justify-center animate-bounce">
<span className="text-3xl sm:text-4xl">🎉</span>
</div>
</div>
<h2 className="text-2xl sm:text-3xl font-bold gradient-text mb-2">
Your Audiobook is Ready!
</h2>
<p className="text-gray-600 text-sm sm:text-base">
Listen below or download to enjoy offline
</p>
</div>
<div className="bg-white rounded-lg shadow-md p-4 sm:p-6 mb-4 sm:mb-6"> {/* Audio Player Card */}
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">Listen to your audiobook</h3> <div className="bg-gradient-to-br from-purple-50 to-indigo-50 rounded-2xl shadow-lg p-5 sm:p-8 mb-6 border-2 border-purple-200">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 bg-gradient-to-br from-purple-500 to-indigo-600 rounded-xl flex items-center justify-center shadow-md">
<span className="text-xl">🎧</span>
</div>
<h3 className="text-lg sm:text-xl font-bold text-gray-800">Listen Now</h3>
</div>
<audio <audio
controls controls
src={audioUrl} src={audioUrl}
className="w-full min-h-[44px]" className="w-full min-h-[44px] rounded-xl shadow-md"
preload="metadata" preload="metadata"
style={{
filter: 'drop-shadow(0 4px 6px rgba(0, 0, 0, 0.1))'
}}
> >
Your browser does not support the audio element. Your browser does not support the audio element.
</audio> </audio>
</div> </div>
{/* Download Button */}
<div className="flex justify-center"> <div className="flex justify-center">
<button <button
onClick={onDownload} onClick={onDownload}
className="w-full sm:w-auto px-6 py-3 min-h-[44px] bg-green-600 hover:bg-green-700 active:bg-green-800 text-white font-semibold rounded-lg transition-colors cursor-pointer touch-manipulation" className="w-full sm:w-auto px-8 py-4 min-h-[44px] bg-gradient-to-r from-green-500 to-emerald-600 hover:from-green-600 hover:to-emerald-700 text-white font-bold rounded-xl transition-all cursor-pointer touch-manipulation shadow-lg text-base sm:text-lg"
> >
Download audiobook <span className="flex items-center justify-center gap-3">
<span className="text-xl">⬇️</span>
Download Audiobook
</span>
</button> </button>
</div> </div>
{/* Success Message */}
<div className="mt-6 p-4 bg-gradient-to-r from-green-50 to-emerald-50 border-2 border-green-200 text-green-800 rounded-xl shadow-sm text-center text-sm sm:text-base">
<p className="font-semibold">
✅ Your audiobook has been successfully generated!
</p>
</div>
</div> </div>
); );
} }

View File

@@ -22,35 +22,40 @@ export default function ScriptReviewSection({
}; };
return ( return (
<div className="w-full max-w-4xl mx-auto p-4 sm:p-6"> <div className="w-full max-w-4xl mx-auto p-4 sm:p-6 glass-effect rounded-2xl shadow-glow">
<h2 className="text-xl sm:text-2xl font-bold mb-3 sm:mb-4">Review Script</h2> <div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 bg-gradient-to-br from-purple-500 to-indigo-600 rounded-xl flex items-center justify-center shadow-lg">
<span className="text-xl">📋</span>
</div>
<h2 className="text-xl sm:text-2xl font-bold gradient-text">Review Script</h2>
</div>
<p className="text-sm sm:text-base text-gray-600 mb-4 sm:mb-6"> <p className="text-sm sm:text-base text-gray-700 mb-6 bg-purple-50 p-3 rounded-xl border-2 border-purple-100">
Review the AI-generated script and adjust speaker assignments as needed. Review the AI-generated script and adjust speaker assignments as needed.
</p> </p>
{/* Script Segments */} {/* Script Segments */}
<div className="space-y-4 sm:space-y-6 mb-6 sm:mb-8"> <div className="space-y-4 sm:space-y-5 mb-6 sm:mb-8">
{segments.map((segment, index) => ( {segments.map((segment, index) => (
<div <div
key={segment.id} key={segment.id}
className="border-2 border-gray-300 rounded-lg p-3 sm:p-4 bg-white shadow-sm" className="border-2 border-purple-200 rounded-xl p-4 sm:p-5 bg-white shadow-md hover:shadow-lg transition-all"
> >
{/* Segment Header */} {/* Segment Header */}
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4 mb-3"> <div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4 mb-3">
<span className="text-xs sm:text-sm font-semibold text-gray-500"> <span className="inline-flex items-center justify-center w-20 px-2 py-1 text-xs sm:text-sm font-bold text-purple-700 bg-purple-100 rounded-lg">
Segment {index + 1} #{index + 1}
</span> </span>
{/* Speaker Dropdown */} {/* Speaker Dropdown */}
<div className="flex items-center gap-2 flex-1"> <div className="flex items-center gap-2 flex-1">
<label className="text-xs sm:text-sm font-medium text-gray-700 whitespace-nowrap"> <label className="text-xs sm:text-sm font-semibold text-gray-700 whitespace-nowrap">
Speaker: 🎤 Speaker:
</label> </label>
<select <select
value={segment.speaker} value={segment.speaker}
onChange={(e) => handleSpeakerChange(segment.id, e.target.value)} onChange={(e) => handleSpeakerChange(segment.id, e.target.value)}
className="flex-1 p-2 sm:p-2 min-h-[44px] border-2 border-gray-300 rounded-lg focus:border-blue-500 focus:outline-none text-sm sm:text-base touch-manipulation" className="flex-1 p-3 min-h-[44px] border-2 border-purple-200 rounded-xl focus:border-purple-500 focus:ring-4 focus:ring-purple-200 focus:outline-none text-sm sm:text-base touch-manipulation bg-white shadow-sm"
disabled={isGenerating} disabled={isGenerating}
> >
{roles.map((role) => ( {roles.map((role) => (
@@ -63,7 +68,7 @@ export default function ScriptReviewSection({
</div> </div>
{/* Segment Text */} {/* Segment Text */}
<div className="w-full min-h-[80px] p-3 border-2 border-gray-200 rounded-lg bg-gray-50 text-sm sm:text-base whitespace-pre-wrap"> <div className="w-full min-h-[80px] p-4 border-2 border-purple-100 rounded-xl bg-gradient-to-br from-purple-50 to-indigo-50 text-sm sm:text-base whitespace-pre-wrap shadow-inner">
{segment.text} {segment.text}
</div> </div>
</div> </div>
@@ -75,10 +80,10 @@ export default function ScriptReviewSection({
<button <button
onClick={onSkipReview} onClick={onSkipReview}
disabled={isGenerating} disabled={isGenerating}
className={`w-full sm:w-auto px-6 py-3 min-h-[44px] rounded-lg font-semibold transition-colors touch-manipulation ${ className={`w-full sm:w-auto px-6 py-4 min-h-[44px] rounded-xl font-semibold transition-all touch-manipulation shadow-lg ${
isGenerating isGenerating
? 'bg-gray-300 text-gray-500 cursor-not-allowed' ? 'bg-gray-300 text-gray-500 cursor-not-allowed opacity-60'
: 'bg-gray-500 text-white hover:bg-gray-600 active:bg-gray-700 cursor-pointer' : 'bg-gradient-to-r from-gray-500 to-gray-600 text-white hover:from-gray-600 hover:to-gray-700 cursor-pointer'
}`} }`}
> >
Skip Review Skip Review
@@ -87,20 +92,31 @@ export default function ScriptReviewSection({
<button <button
onClick={onGenerate} onClick={onGenerate}
disabled={isGenerating} disabled={isGenerating}
className={`w-full sm:w-auto px-8 py-3 min-h-[44px] rounded-lg font-semibold text-white transition-colors touch-manipulation ${ className={`w-full sm:w-auto px-8 py-4 min-h-[44px] rounded-xl font-semibold text-white transition-all touch-manipulation shadow-lg ${
isGenerating isGenerating
? 'bg-gray-400 cursor-not-allowed' ? 'bg-gray-400 cursor-not-allowed opacity-60'
: 'bg-green-600 hover:bg-green-700 active:bg-green-800 cursor-pointer' : 'bg-gradient-to-r from-green-500 to-emerald-600 hover:from-green-600 hover:to-emerald-700 cursor-pointer'
}`} }`}
> >
{isGenerating ? 'Generating...' : 'Generate Audiobook'} {isGenerating ? (
<span className="flex items-center justify-center gap-2">
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
Generating...
</span>
) : (
<span className="flex items-center justify-center gap-2">
✨ Generate Audiobook
</span>
)}
</button> </button>
</div> </div>
{/* Info Message */} {/* Info Message */}
{!isGenerating && ( {!isGenerating && (
<div className="mt-4 sm:mt-6 p-3 sm:p-4 bg-blue-50 border border-blue-200 text-blue-800 rounded text-sm sm:text-base"> <div className="mt-6 p-4 bg-gradient-to-r from-blue-50 to-indigo-50 border-2 border-blue-200 text-blue-800 rounded-xl shadow-sm text-sm sm:text-base">
<p className="font-semibold mb-1">💡 Tip:</p> <p className="font-bold mb-2 flex items-center gap-2">
<span className="text-lg">💡</span> Pro Tip:
</p>
<p> <p>
Review the AI-generated speaker assignments and make any necessary corrections before generating the audiobook. Review the AI-generated speaker assignments and make any necessary corrections before generating the audiobook.
</p> </p>

View File

@@ -36,35 +36,50 @@ export default function TextInputSection({
const isButtonDisabled = disabled || isLoading || text.trim() === ''; const isButtonDisabled = disabled || isLoading || text.trim() === '';
return ( return (
<div className="w-full max-w-4xl mx-auto p-4 sm:p-6"> <div className="w-full max-w-4xl mx-auto p-4 sm:p-6 glass-effect rounded-2xl shadow-glow">
<h2 className="text-xl sm:text-2xl font-bold mb-3 sm:mb-4">Enter Your Text</h2> <div className="flex items-center gap-3 mb-4 sm:mb-6">
<div className="w-10 h-10 bg-gradient-to-br from-purple-500 to-indigo-600 rounded-xl flex items-center justify-center shadow-lg">
<span className="text-xl">📝</span>
</div>
<h2 className="text-xl sm:text-2xl font-bold gradient-text">Enter Your Text</h2>
</div>
<div className="mb-4"> <div className="mb-4">
<textarea <textarea
value={text} value={text}
onChange={handleTextChange} onChange={handleTextChange}
placeholder="Enter your text here. Use patterns like 'Name:' or '[Name]' to mark different speakers..." placeholder="Enter your text here. Use patterns like 'Name:' or '[Name]' to mark different speakers..."
className="w-full h-64 sm:h-80 md:h-96 p-3 sm:p-4 border-2 border-gray-300 rounded-lg focus:border-blue-500 focus:outline-none resize-y text-sm sm:text-base touch-manipulation" className="w-full h-64 sm:h-80 md:h-96 p-4 sm:p-5 border-2 border-purple-200 rounded-xl focus:border-purple-500 focus:ring-4 focus:ring-purple-200 focus:outline-none resize-y text-sm sm:text-base touch-manipulation shadow-sm"
disabled={disabled || isLoading} disabled={disabled || isLoading}
/> />
</div> </div>
{showValidation && text.trim() === '' && ( {showValidation && text.trim() === '' && (
<div className="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded text-sm sm:text-base"> <div className="mb-4 p-3 sm:p-4 bg-red-50 border-2 border-red-300 text-red-700 rounded-xl shadow-sm text-sm sm:text-base">
Please enter some text before proceeding. <span className="font-semibold">⚠️</span> Please enter some text before proceeding.
</div> </div>
)} )}
<button <button
onClick={handleNext} onClick={handleNext}
disabled={isButtonDisabled} disabled={isButtonDisabled}
className={`w-full sm:w-auto px-6 py-3 min-h-[44px] rounded-lg font-semibold text-white transition-colors touch-manipulation ${ className={`w-full sm:w-auto px-8 py-4 min-h-[44px] rounded-xl font-semibold text-white transition-all touch-manipulation shadow-lg ${
isButtonDisabled isButtonDisabled
? 'bg-gray-400 cursor-not-allowed' ? 'bg-gray-400 cursor-not-allowed opacity-60'
: 'bg-blue-600 hover:bg-blue-700 active:bg-blue-800 cursor-pointer' : 'bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 cursor-pointer'
}`} }`}
> >
{isLoading ? 'Processing...' : 'Next: Assign voices'} {isLoading ? (
<span className="flex items-center justify-center gap-2">
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
Processing...
</span>
) : (
<span className="flex items-center justify-center gap-2">
Next: Assign voices
<span>→</span>
</span>
)}
</button> </button>
</div> </div>
); );

View File

@@ -159,22 +159,29 @@ export default function VoiceAssignmentSection({
const isGenerateDisabled = !allRolesAssigned || isGenerating; const isGenerateDisabled = !allRolesAssigned || isGenerating;
return ( return (
<div className="w-full max-w-4xl mx-auto p-4 sm:p-6"> <div className="w-full max-w-4xl mx-auto p-4 sm:p-6 glass-effect rounded-2xl shadow-glow">
<h2 className="text-xl sm:text-2xl font-bold mb-4 sm:mb-6">Assign voices to roles</h2> <div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 bg-gradient-to-br from-purple-500 to-indigo-600 rounded-xl flex items-center justify-center shadow-lg">
<span className="text-xl">🎤</span>
</div>
<h2 className="text-xl sm:text-2xl font-bold gradient-text">Assign Voices to Roles</h2>
</div>
{/* Role-to-Voice Mapping Section */} {/* Role-to-Voice Mapping Section */}
<div className="mb-6 sm:mb-8"> <div className="mb-6 sm:mb-8">
<h3 className="text-lg sm:text-xl font-semibold mb-3 sm:mb-4">Voice Assignments</h3> <h3 className="text-lg sm:text-xl font-semibold mb-4 text-gray-800 flex items-center gap-2">
<span>👥</span> Voice Assignments
</h3>
<div className="space-y-3 sm:space-y-4"> <div className="space-y-3 sm:space-y-4">
{roles.map((role) => ( {roles.map((role) => (
<div key={role.id} className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4"> <div key={role.id} className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4 bg-white p-4 rounded-xl shadow-sm border-2 border-purple-100 hover:border-purple-300 transition-all">
<label className="font-medium text-gray-700 text-sm sm:text-base sm:w-40"> <label className="font-semibold text-gray-700 text-sm sm:text-base sm:w-40">
{role.label}: {role.label}:
</label> </label>
<select <select
value={voiceAssignments.get(role.id) || ''} value={voiceAssignments.get(role.id) || ''}
onChange={(e) => handleVoiceSelect(role.id, e.target.value)} onChange={(e) => handleVoiceSelect(role.id, e.target.value)}
className="flex-1 p-3 sm:p-2 min-h-[44px] border-2 border-gray-300 rounded-lg focus:border-blue-500 focus:outline-none text-sm sm:text-base touch-manipulation" className="flex-1 p-3 min-h-[44px] border-2 border-purple-200 rounded-xl focus:border-purple-500 focus:ring-4 focus:ring-purple-200 focus:outline-none text-sm sm:text-base touch-manipulation bg-white shadow-sm"
> >
<option value="">Select a voice...</option> <option value="">Select a voice...</option>
{KOKORO_VOICES.map((voice) => ( {KOKORO_VOICES.map((voice) => (
@@ -189,12 +196,14 @@ export default function VoiceAssignmentSection({
</div> </div>
{/* Language Selector */} {/* Language Selector */}
<div className="mb-6"> <div className="mb-6 bg-white p-4 rounded-xl shadow-sm border-2 border-purple-100">
<h3 className="text-lg sm:text-xl font-semibold mb-3 sm:mb-4">Language</h3> <h3 className="text-lg sm:text-xl font-semibold mb-3 text-gray-800 flex items-center gap-2">
<span>🌍</span> Language
</h3>
<select <select
value={language} value={language}
onChange={handleLanguageSelect} onChange={handleLanguageSelect}
className="w-full p-3 sm:p-2 min-h-[44px] border-2 border-gray-300 rounded-lg focus:border-blue-500 focus:outline-none text-sm sm:text-base touch-manipulation" className="w-full p-3 min-h-[44px] border-2 border-purple-200 rounded-xl focus:border-purple-500 focus:ring-4 focus:ring-purple-200 focus:outline-none text-sm sm:text-base touch-manipulation bg-white shadow-sm"
> >
{LANGUAGES.map((lang) => ( {LANGUAGES.map((lang) => (
<option key={lang.code} value={lang.code}> <option key={lang.code} value={lang.code}>
@@ -205,10 +214,12 @@ export default function VoiceAssignmentSection({
</div> </div>
{/* Speed Slider */} {/* Speed Slider */}
<div className="mb-6 sm:mb-8"> <div className="mb-6 sm:mb-8 bg-white p-4 rounded-xl shadow-sm border-2 border-purple-100">
<h3 className="text-lg sm:text-xl font-semibold mb-3 sm:mb-4">Playback Speed</h3> <h3 className="text-lg sm:text-xl font-semibold mb-4 text-gray-800 flex items-center gap-2">
<div className="flex items-center gap-2 sm:gap-4"> <span>⚡</span> Playback Speed
<span className="text-xs sm:text-sm text-gray-600 w-10 sm:w-12">0.5x</span> </h3>
<div className="flex items-center gap-3 sm:gap-4">
<span className="text-xs sm:text-sm text-gray-600 w-10 sm:w-12 font-medium">0.5x</span>
<input <input
type="range" type="range"
min="0.5" min="0.5"
@@ -218,10 +229,12 @@ export default function VoiceAssignmentSection({
onChange={handleSpeedChange} onChange={handleSpeedChange}
className="flex-1 h-8 sm:h-auto touch-manipulation" className="flex-1 h-8 sm:h-auto touch-manipulation"
/> />
<span className="text-xs sm:text-sm text-gray-600 w-10 sm:w-12">1.5x</span> <span className="text-xs sm:text-sm text-gray-600 w-10 sm:w-12 font-medium">1.5x</span>
<span className="font-semibold text-gray-800 w-12 sm:w-16 text-center text-sm sm:text-base"> <div className="w-16 sm:w-20 text-center">
{speed.toFixed(1)}x <span className="font-bold text-purple-600 text-base sm:text-lg bg-purple-50 px-3 py-1 rounded-lg">
</span> {speed.toFixed(1)}x
</span>
</div>
</div> </div>
</div> </div>
@@ -230,34 +243,46 @@ export default function VoiceAssignmentSection({
<button <button
onClick={onGenerate} onClick={onGenerate}
disabled={isGenerateDisabled} disabled={isGenerateDisabled}
className={`w-full sm:w-auto px-8 py-3 min-h-[44px] rounded-lg font-semibold text-white transition-colors touch-manipulation ${ className={`w-full sm:w-auto px-8 py-4 min-h-[44px] rounded-xl font-semibold text-white transition-all touch-manipulation shadow-lg ${
isGenerateDisabled isGenerateDisabled
? 'bg-gray-400 cursor-not-allowed' ? 'bg-gray-400 cursor-not-allowed opacity-60'
: 'bg-blue-600 hover:bg-blue-700 active:bg-blue-800 cursor-pointer' : 'bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 cursor-pointer'
}`} }`}
> >
{isGenerating ? 'Generating Script...' : 'Next: Review Script'} {isGenerating ? (
<span className="flex items-center justify-center gap-2">
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
Generating Script...
</span>
) : (
<span className="flex items-center justify-center gap-2">
Next: Review Script
<span>→</span>
</span>
)}
</button> </button>
{onSkipReview && ( {onSkipReview && (
<button <button
onClick={onSkipReview} onClick={onSkipReview}
disabled={isGenerateDisabled} disabled={isGenerateDisabled}
className={`w-full sm:w-auto px-8 py-3 min-h-[44px] rounded-lg font-semibold text-white transition-colors touch-manipulation ${ className={`w-full sm:w-auto px-8 py-4 min-h-[44px] rounded-xl font-semibold text-white transition-all touch-manipulation shadow-lg ${
isGenerateDisabled isGenerateDisabled
? 'bg-gray-400 cursor-not-allowed' ? 'bg-gray-400 cursor-not-allowed opacity-60'
: 'bg-green-600 hover:bg-green-700 active:bg-green-800 cursor-pointer' : 'bg-gradient-to-r from-green-500 to-emerald-600 hover:from-green-600 hover:to-emerald-700 cursor-pointer'
}`} }`}
> >
Skip Review & Generate <span className="flex items-center justify-center gap-2">
⚡ Skip Review & Generate
</span>
</button> </button>
)} )}
</div> </div>
{/* Validation Message */} {/* Validation Message */}
{!allRolesAssigned && !isGenerating && ( {!allRolesAssigned && !isGenerating && (
<div className="mt-4 p-3 bg-yellow-100 border border-yellow-400 text-yellow-800 rounded text-center text-sm sm:text-base"> <div className="mt-4 p-4 bg-yellow-50 border-2 border-yellow-300 text-yellow-800 rounded-xl text-center text-sm sm:text-base shadow-sm">
Please assign voices to all roles before generating the audiobook. <span className="font-semibold">⚠️</span> Please assign voices to all roles before generating the audiobook.
</div> </div>
)} )}
</div> </div>

View File

@@ -3,8 +3,8 @@
@tailwind utilities; @tailwind utilities;
:root { :root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
line-height: 1.5; line-height: 1.6;
font-weight: 400; font-weight: 400;
} }
@@ -12,4 +12,25 @@ body {
margin: 0; margin: 0;
min-width: 320px; min-width: 320px;
min-height: 100vh; min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background-attachment: fixed;
}
@layer utilities {
.glass-effect {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.gradient-text {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.shadow-glow {
box-shadow: 0 10px 40px rgba(102, 126, 234, 0.2);
}
} }

86
test_api_directly.py Normal file
View File

@@ -0,0 +1,86 @@
#!/usr/bin/env python3
"""
Test the API directly with the full story to verify backend functionality.
"""
import requests
import json
# Read the full story
with open('Test Story.txt', 'r') as f:
story = f.read()
print(f"Story length: {len(story)} characters\n")
# Test 1: Role Detection
print("=" * 70)
print("TEST 1: Role Detection")
print("=" * 70)
response = requests.post(
'http://localhost:8002/api/tts/roles',
json={'text': story},
headers={'Content-Type': 'application/json'}
)
print(f"Status: {response.status_code}")
if response.status_code == 200:
roles_data = response.json()
roles = roles_data['roles']
print(f"✓ Detected {len(roles)} roles:")
for role in roles:
print(f" - {role['label']}")
else:
print(f"❌ Error: {response.text}")
exit(1)
print()
# Test 2: Audio Generation with Multiple Voices
print("=" * 70)
print("TEST 2: Audio Generation")
print("=" * 70)
# Assign different voices to each role
voice_map = {
'Lily': 'af_bella',
'Benny': 'am_adam',
'Sammy': 'am_michael',
'Narrator': 'af_sarah'
}
# Build roles array for API
roles_array = []
for role in roles:
role_id = role['id']
voice = voice_map.get(role_id, 'af_bella')
roles_array.append({'id': role_id, 'voice': voice})
print(f" {role_id} → {voice}")
print(f"\nGenerating audiobook...")
response = requests.post(
'http://localhost:8002/api/tts',
json={
'text': story,
'lang': 'a',
'roles': roles_array,
'speed': 1.0
},
headers={'Content-Type': 'application/json'}
)
print(f"Status: {response.status_code}")
if response.status_code == 200:
# Save the audio file
with open('test_output.wav', 'wb') as f:
f.write(response.content)
print(f"✓ Generated audiobook: {len(response.content)} bytes")
print(f"✓ Saved to: test_output.wav")
print("\nYou can now play test_output.wav to hear the multi-voice audiobook!")
else:
print(f"❌ Error: {response.text}")
exit(1)
print("\n" + "=" * 70)
print("SUCCESS! Backend is working correctly with full story.")
print("=" * 70)

0
test_full_workflow.py Normal file
View File

64
test_my_story_full.py Normal file
View File

@@ -0,0 +1,64 @@
#!/usr/bin/env python3
import sys
sys.path.insert(0, 'backend')
import asyncio
from services.role_detector import RoleDetector
from services.tts_service import TTSService
from services.ollama_service import OllamaService
async def test():
# Read the actual story
with open('Test Story.txt', 'r') as f:
story = f.read()
print(f'Story length: {len(story)} characters\n')
# Initialize services
ollama_service = OllamaService()
role_detector = RoleDetector(ollama_service=ollama_service)
tts_service = TTSService(ollama_service=ollama_service)
# Step 1: Detect roles
print('Step 1: Detecting roles...')
roles = await role_detector.detect_roles(story)
print(f'Detected {len(roles)} roles: {[r["label"] for r in roles]}\n')
# Step 2: Assign voices
print('Step 2: Assigning voices...')
role_voice_map = {
'Lily': 'af_bella',
'Benny': 'am_adam',
'Sammy': 'am_michael',
'Narrator': 'af_sarah'
}
for role, voice in role_voice_map.items():
print(f' {role} → {voice}')
print()
# Step 3: Test AI segmentation
print('Step 3: Testing AI segmentation...')
segments = await tts_service.segment_text_with_ai(story, role_voice_map)
if segments is None:
print('❌ AI segmentation returned None!')
print('Falling back to pattern-based segmentation...')
segments = tts_service.segment_text(story, role_voice_map)
print(f'Generated {len(segments)} segments\n')
# Analyze segments
speaker_counts = {}
for speaker, voice, text in segments:
speaker_counts[speaker] = speaker_counts.get(speaker, 0) + 1
print('Segments per speaker:')
for speaker, count in sorted(speaker_counts.items(), key=lambda x: x[1], reverse=True):
print(f' {speaker}: {count} segments')
print('\nFirst 5 segments:')
for i, (speaker, voice, text) in enumerate(segments[:5], 1):
preview = text[:80] + '...' if len(text) > 80 else text
print(f' {i}. [{speaker}] {preview}')
asyncio.run(test())

BIN
test_output.wav Normal file

Binary file not shown.