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 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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
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;
|
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 */
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
|
||||||
<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
|
<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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
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