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:
154
.kiro/specs/text-to-audiobook/SCRIPT_REVIEW_FEATURE.md
Normal file
154
.kiro/specs/text-to-audiobook/SCRIPT_REVIEW_FEATURE.md
Normal 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
|
||||
@@ -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 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
|
||||
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
|
||||
|
||||
@@ -93,10 +94,12 @@ Main application container managing the overall workflow state.
|
||||
- `text: string` - User input text
|
||||
- `roles: Role[]` - Detected roles from backend
|
||||
- `voiceAssignments: Map<string, string>` - Role ID to Voice ID mapping
|
||||
- `scriptSegments: ScriptSegment[]` - AI-generated script segments
|
||||
- `selectedLanguage: string` - Language code (default: 'a')
|
||||
- `speed: number` - Playback speed (default: 1.0)
|
||||
- `audioUrl: string | null` - Generated audio blob URL
|
||||
- `isDetectingRoles: boolean` - Loading state for role detection
|
||||
- `isGeneratingScript: boolean` - Loading state for script generation
|
||||
- `isGenerating: boolean` - Loading state for audio generation
|
||||
- `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
|
||||
- `handleDetectRoles()` - Calls API to detect roles
|
||||
- `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
|
||||
|
||||
#### 2. TextInputSection Component
|
||||
@@ -132,14 +138,25 @@ Displays role-to-voice mapping interface with language and speed controls.
|
||||
- `isGenerating: 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.
|
||||
|
||||
**Props:**
|
||||
- `audioUrl: string`
|
||||
- `onDownload: () => void`
|
||||
|
||||
#### 5. API Client (`src/api/tts.ts`)
|
||||
#### 6. API Client (`src/api/tts.ts`)
|
||||
|
||||
**Interfaces:**
|
||||
```typescript
|
||||
@@ -156,8 +173,24 @@ export interface RolesResponse {
|
||||
roles: Role[];
|
||||
}
|
||||
|
||||
export interface GenerateRequest {
|
||||
export interface ScriptSegment {
|
||||
id: string;
|
||||
speaker: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface ScriptRequest {
|
||||
text: string;
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
export interface ScriptResponse {
|
||||
segments: ScriptSegment[];
|
||||
}
|
||||
|
||||
export interface GenerateRequest {
|
||||
segments?: ScriptSegment[];
|
||||
text?: string;
|
||||
lang: string;
|
||||
roles: { id: string; voice: string }[];
|
||||
speed?: number;
|
||||
@@ -167,6 +200,7 @@ export interface GenerateRequest {
|
||||
**Functions:**
|
||||
```typescript
|
||||
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 checkHealth(): Promise<{ status: string }>
|
||||
```
|
||||
@@ -188,6 +222,11 @@ async def detect_roles(request: RoleDetectionRequest) -> RolesResponse:
|
||||
"""Analyzes text and returns detected roles"""
|
||||
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")
|
||||
async def generate_audiobook(request: TTSRequest) -> StreamingResponse:
|
||||
"""Generates multi-voice audiobook and streams audio"""
|
||||
@@ -206,12 +245,25 @@ class RoleInfo(BaseModel):
|
||||
class RolesResponse(BaseModel):
|
||||
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):
|
||||
id: str
|
||||
voice: str
|
||||
|
||||
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
|
||||
roles: List[RoleVoiceMapping]
|
||||
speed: Optional[float] = 1.0
|
||||
@@ -384,10 +436,11 @@ interface LanguageOption {
|
||||
**Application State:**
|
||||
```typescript
|
||||
interface AppState {
|
||||
phase: 'input' | 'assignment' | 'generated';
|
||||
phase: 'input' | 'assignment' | 'review' | 'generated';
|
||||
text: string;
|
||||
roles: Role[];
|
||||
voiceAssignments: Map<string, string>;
|
||||
scriptSegments: ScriptSegment[];
|
||||
language: string;
|
||||
speed: number;
|
||||
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.
|
||||
**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
|
||||
|
||||
### Frontend Error Handling
|
||||
|
||||
@@ -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
|
||||
- **Segment**: A portion of text assigned to a specific role for synthesis
|
||||
- **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
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
### 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
|
||||
|
||||
@@ -443,7 +443,7 @@
|
||||
- Test skip review bypasses review phase
|
||||
- _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"
|
||||
- Add "Skip Review & Generate" button as alternative
|
||||
- Update button handlers to call appropriate methods
|
||||
|
||||
18
backend/test_ollama_quick.py
Normal file
18
backend/test_ollama_quick.py
Normal 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
79
diagnose_issue.md
Normal 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
|
||||
@@ -10,6 +10,25 @@
|
||||
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 */
|
||||
input[type="range"] {
|
||||
-webkit-appearance: none;
|
||||
@@ -17,9 +36,10 @@ input[type="range"] {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
background: #d1d5db;
|
||||
background: linear-gradient(to right, #667eea, #764ba2);
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
input[type="range"]::-webkit-slider-thumb {
|
||||
@@ -28,35 +48,39 @@ input[type="range"]::-webkit-slider-thumb {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: #2563eb;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
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 {
|
||||
background: #1d4ed8;
|
||||
transform: scale(1.2);
|
||||
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.6);
|
||||
}
|
||||
|
||||
input[type="range"]::-webkit-slider-thumb:active {
|
||||
background: #1e40af;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-thumb {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: #2563eb;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
cursor: pointer;
|
||||
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 {
|
||||
background: #1d4ed8;
|
||||
transform: scale(1.2);
|
||||
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.6);
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-thumb:active {
|
||||
background: #1e40af;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Ensure audio controls are touch-friendly on mobile */
|
||||
|
||||
@@ -32,6 +32,7 @@ function App() {
|
||||
try {
|
||||
// Detect roles in the text
|
||||
const response = await detectRoles(text)
|
||||
console.log('Detected roles:', response.roles)
|
||||
setRoles(response.roles)
|
||||
|
||||
// Initialize voice assignments map (empty initially)
|
||||
@@ -157,15 +158,24 @@ function App() {
|
||||
const allRolesAssigned = roles.length > 0 && roles.every(role => voiceAssignments.has(role.id))
|
||||
|
||||
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">
|
||||
<h1 className="text-2xl sm:text-3xl md:text-4xl font-bold text-center mb-4 sm:mb-6 md:mb-8">
|
||||
Text to Audiobook
|
||||
</h1>
|
||||
{/* Header with gradient text and icon */}
|
||||
<div className="text-center mb-6 sm:mb-10">
|
||||
<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 && (
|
||||
<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">
|
||||
{error}
|
||||
<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">
|
||||
<span className="font-semibold">⚠️ Error:</span> {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -186,6 +196,7 @@ function App() {
|
||||
Generation may take several minutes for very long content.
|
||||
</div>
|
||||
)}
|
||||
{console.log('Rendering VoiceAssignmentSection with roles:', roles, 'allRolesAssigned:', allRolesAssigned)}
|
||||
<VoiceAssignmentSection
|
||||
roles={roles}
|
||||
voiceAssignments={voiceAssignments}
|
||||
@@ -228,14 +239,20 @@ function App() {
|
||||
)}
|
||||
|
||||
{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">
|
||||
Generation may take several minutes for very long content. Please be patient...
|
||||
<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">
|
||||
<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>
|
||||
)}
|
||||
|
||||
{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">
|
||||
Generating script preview... This may take a moment.
|
||||
<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">
|
||||
<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>
|
||||
|
||||
@@ -8,29 +8,63 @@ export default function AudioPlayerSection({
|
||||
onDownload
|
||||
}: AudioPlayerSectionProps) {
|
||||
return (
|
||||
<div className="w-full max-w-4xl mx-auto p-4 sm:p-6">
|
||||
<h2 className="text-xl sm:text-2xl font-bold mb-4 sm:mb-6">Your Audiobook is Ready!</h2>
|
||||
<div className="w-full max-w-4xl mx-auto p-4 sm:p-6 glass-effect rounded-2xl shadow-glow">
|
||||
{/* 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">
|
||||
<h3 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">Listen to your audiobook</h3>
|
||||
{/* Audio Player Card */}
|
||||
<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
|
||||
controls
|
||||
src={audioUrl}
|
||||
className="w-full min-h-[44px]"
|
||||
className="w-full min-h-[44px] rounded-xl shadow-md"
|
||||
preload="metadata"
|
||||
style={{
|
||||
filter: 'drop-shadow(0 4px 6px rgba(0, 0, 0, 0.1))'
|
||||
}}
|
||||
>
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
</div>
|
||||
|
||||
{/* Download Button */}
|
||||
<div className="flex justify-center">
|
||||
<button
|
||||
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>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,35 +22,40 @@ export default function ScriptReviewSection({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-4xl mx-auto p-4 sm:p-6">
|
||||
<h2 className="text-xl sm:text-2xl font-bold mb-3 sm:mb-4">Review Script</h2>
|
||||
<div className="w-full max-w-4xl mx-auto p-4 sm:p-6 glass-effect rounded-2xl shadow-glow">
|
||||
<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.
|
||||
</p>
|
||||
|
||||
{/* 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) => (
|
||||
<div
|
||||
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 */}
|
||||
<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">
|
||||
Segment {index + 1}
|
||||
<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">
|
||||
#{index + 1}
|
||||
</span>
|
||||
|
||||
{/* Speaker Dropdown */}
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<label className="text-xs sm:text-sm font-medium text-gray-700 whitespace-nowrap">
|
||||
Speaker:
|
||||
<label className="text-xs sm:text-sm font-semibold text-gray-700 whitespace-nowrap">
|
||||
🎤 Speaker:
|
||||
</label>
|
||||
<select
|
||||
value={segment.speaker}
|
||||
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}
|
||||
>
|
||||
{roles.map((role) => (
|
||||
@@ -63,7 +68,7 @@ export default function ScriptReviewSection({
|
||||
</div>
|
||||
|
||||
{/* 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}
|
||||
</div>
|
||||
</div>
|
||||
@@ -75,10 +80,10 @@ export default function ScriptReviewSection({
|
||||
<button
|
||||
onClick={onSkipReview}
|
||||
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
|
||||
? 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
||||
: 'bg-gray-500 text-white hover:bg-gray-600 active:bg-gray-700 cursor-pointer'
|
||||
? 'bg-gray-300 text-gray-500 cursor-not-allowed opacity-60'
|
||||
: 'bg-gradient-to-r from-gray-500 to-gray-600 text-white hover:from-gray-600 hover:to-gray-700 cursor-pointer'
|
||||
}`}
|
||||
>
|
||||
Skip Review
|
||||
@@ -87,20 +92,31 @@ export default function ScriptReviewSection({
|
||||
<button
|
||||
onClick={onGenerate}
|
||||
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
|
||||
? 'bg-gray-400 cursor-not-allowed'
|
||||
: 'bg-green-600 hover:bg-green-700 active:bg-green-800 cursor-pointer'
|
||||
? 'bg-gray-400 cursor-not-allowed opacity-60'
|
||||
: '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>
|
||||
</div>
|
||||
|
||||
{/* Info Message */}
|
||||
{!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">
|
||||
<p className="font-semibold mb-1">💡 Tip:</p>
|
||||
<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-bold mb-2 flex items-center gap-2">
|
||||
<span className="text-lg">💡</span> Pro Tip:
|
||||
</p>
|
||||
<p>
|
||||
Review the AI-generated speaker assignments and make any necessary corrections before generating the audiobook.
|
||||
</p>
|
||||
|
||||
@@ -36,35 +36,50 @@ export default function TextInputSection({
|
||||
const isButtonDisabled = disabled || isLoading || text.trim() === '';
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-4xl mx-auto p-4 sm:p-6">
|
||||
<h2 className="text-xl sm:text-2xl font-bold mb-3 sm:mb-4">Enter Your Text</h2>
|
||||
<div className="w-full max-w-4xl mx-auto p-4 sm:p-6 glass-effect rounded-2xl shadow-glow">
|
||||
<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">
|
||||
<textarea
|
||||
value={text}
|
||||
onChange={handleTextChange}
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{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">
|
||||
Please enter some text before proceeding.
|
||||
<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">
|
||||
<span className="font-semibold">⚠️</span> Please enter some text before proceeding.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={handleNext}
|
||||
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
|
||||
? 'bg-gray-400 cursor-not-allowed'
|
||||
: 'bg-blue-600 hover:bg-blue-700 active:bg-blue-800 cursor-pointer'
|
||||
? 'bg-gray-400 cursor-not-allowed opacity-60'
|
||||
: '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>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -159,22 +159,29 @@ export default function VoiceAssignmentSection({
|
||||
const isGenerateDisabled = !allRolesAssigned || isGenerating;
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-4xl mx-auto p-4 sm:p-6">
|
||||
<h2 className="text-xl sm:text-2xl font-bold mb-4 sm:mb-6">Assign voices to roles</h2>
|
||||
<div className="w-full max-w-4xl mx-auto p-4 sm:p-6 glass-effect rounded-2xl shadow-glow">
|
||||
<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 */}
|
||||
<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">
|
||||
{roles.map((role) => (
|
||||
<div key={role.id} className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
|
||||
<label className="font-medium text-gray-700 text-sm sm:text-base sm:w-40">
|
||||
<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-semibold text-gray-700 text-sm sm:text-base sm:w-40">
|
||||
{role.label}:
|
||||
</label>
|
||||
<select
|
||||
value={voiceAssignments.get(role.id) || ''}
|
||||
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>
|
||||
{KOKORO_VOICES.map((voice) => (
|
||||
@@ -189,12 +196,14 @@ export default function VoiceAssignmentSection({
|
||||
</div>
|
||||
|
||||
{/* Language Selector */}
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg sm:text-xl font-semibold mb-3 sm:mb-4">Language</h3>
|
||||
<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 text-gray-800 flex items-center gap-2">
|
||||
<span>🌍</span> Language
|
||||
</h3>
|
||||
<select
|
||||
value={language}
|
||||
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) => (
|
||||
<option key={lang.code} value={lang.code}>
|
||||
@@ -205,10 +214,12 @@ export default function VoiceAssignmentSection({
|
||||
</div>
|
||||
|
||||
{/* Speed Slider */}
|
||||
<div className="mb-6 sm:mb-8">
|
||||
<h3 className="text-lg sm:text-xl font-semibold mb-3 sm:mb-4">Playback Speed</h3>
|
||||
<div className="flex items-center gap-2 sm:gap-4">
|
||||
<span className="text-xs sm:text-sm text-gray-600 w-10 sm:w-12">0.5x</span>
|
||||
<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-4 text-gray-800 flex items-center gap-2">
|
||||
<span>⚡</span> Playback Speed
|
||||
</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
|
||||
type="range"
|
||||
min="0.5"
|
||||
@@ -218,10 +229,12 @@ export default function VoiceAssignmentSection({
|
||||
onChange={handleSpeedChange}
|
||||
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="font-semibold text-gray-800 w-12 sm:w-16 text-center text-sm sm:text-base">
|
||||
{speed.toFixed(1)}x
|
||||
</span>
|
||||
<span className="text-xs sm:text-sm text-gray-600 w-10 sm:w-12 font-medium">1.5x</span>
|
||||
<div className="w-16 sm:w-20 text-center">
|
||||
<span className="font-bold text-purple-600 text-base sm:text-lg bg-purple-50 px-3 py-1 rounded-lg">
|
||||
{speed.toFixed(1)}x
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -230,34 +243,46 @@ export default function VoiceAssignmentSection({
|
||||
<button
|
||||
onClick={onGenerate}
|
||||
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
|
||||
? 'bg-gray-400 cursor-not-allowed'
|
||||
: 'bg-blue-600 hover:bg-blue-700 active:bg-blue-800 cursor-pointer'
|
||||
? 'bg-gray-400 cursor-not-allowed opacity-60'
|
||||
: '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>
|
||||
|
||||
{onSkipReview && (
|
||||
<button
|
||||
onClick={onSkipReview}
|
||||
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
|
||||
? 'bg-gray-400 cursor-not-allowed'
|
||||
: 'bg-green-600 hover:bg-green-700 active:bg-green-800 cursor-pointer'
|
||||
? 'bg-gray-400 cursor-not-allowed opacity-60'
|
||||
: '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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Validation Message */}
|
||||
{!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">
|
||||
Please assign voices to all roles before generating the audiobook.
|
||||
<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">
|
||||
<span className="font-semibold">⚠️</span> Please assign voices to all roles before generating the audiobook.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
line-height: 1.6;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@@ -12,4 +12,25 @@ body {
|
||||
margin: 0;
|
||||
min-width: 320px;
|
||||
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
86
test_api_directly.py
Normal 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
0
test_full_workflow.py
Normal file
64
test_my_story_full.py
Normal file
64
test_my_story_full.py
Normal 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
BIN
test_output.wav
Normal file
Binary file not shown.
Reference in New Issue
Block a user