diff --git a/backend/main.py b/backend/main.py index 05626c6..6aa0f33 100644 --- a/backend/main.py +++ b/backend/main.py @@ -9,6 +9,9 @@ from fastapi.responses import JSONResponse from fastapi.exceptions import RequestValidationError from pydantic import BaseModel, Field +# Import services +from services.role_detector import RoleDetector + # Configure logging logging.basicConfig( level=logging.INFO, @@ -91,9 +94,33 @@ async def general_exception_handler(request: Request, exc: Exception): } ) +# Initialize services +role_detector = RoleDetector() + # API Endpoints @app.get("/api/health") async def health_check(): """Health check endpoint.""" logger.info("Health check requested") return {"status": "ok"} + +@app.post("/api/tts/roles", response_model=RolesResponse) +async def detect_roles(request: RoleDetectionRequest): + """ + Analyzes text and returns detected speaker roles. + + Supports two pattern types: + - Colon pattern: "RoleName:" at line start + - Bracket pattern: "[RoleName]" preceding dialogue + + Returns default "Narrator" role if no patterns found. + """ + logger.info(f"Role detection requested for text of length {len(request.text)}") + + try: + roles = role_detector.detect_roles(request.text) + logger.info(f"Detected {len(roles)} roles: {[r['label'] for r in roles]}") + return RolesResponse(roles=roles) + except Exception as e: + logger.error(f"Error during role detection: {str(e)}", exc_info=True) + raise diff --git a/backend/services/role_detector.py b/backend/services/role_detector.py new file mode 100644 index 0000000..5c569fa --- /dev/null +++ b/backend/services/role_detector.py @@ -0,0 +1,93 @@ +""" +Role detection service for identifying distinct speakers in text. +""" +import re +from typing import List, Set, Dict + + +class RoleDetector: + """ + Service for detecting distinct speaker roles in text. + + Supports two pattern types: + 1. Colon pattern: "RoleName:" at the start of a line + 2. Bracket pattern: "[RoleName]" preceding dialogue + """ + + # Regex patterns for role detection + COLON_PATTERN = r'^([A-Z][a-zA-Z\s]+?):\s' + BRACKET_PATTERN = r'\[([A-Z][a-zA-Z\s]+?)\]' + DEFAULT_ROLE = "Narrator" + + def detect_roles(self, text: str) -> List[Dict[str, str]]: + """ + Analyzes text to identify distinct speaker roles. + + Args: + text: Input text to analyze + + Returns: + List of role dictionaries with 'id' and 'label' fields. + Returns default "Narrator" role if no patterns found. + """ + if not text or not text.strip(): + return [{"id": self.DEFAULT_ROLE, "label": self.DEFAULT_ROLE}] + + # Extract roles from both patterns + colon_roles = self._extract_colon_pattern(text) + bracket_roles = self._extract_bracket_pattern(text) + + # Combine and deduplicate + all_roles = colon_roles.union(bracket_roles) + + # If no roles found, return default + if not all_roles: + return [{"id": self.DEFAULT_ROLE, "label": self.DEFAULT_ROLE}] + + # Convert to role dictionaries and sort for consistent ordering + role_list = [ + {"id": role.strip(), "label": role.strip()} + for role in sorted(all_roles) + ] + + return role_list + + def _extract_colon_pattern(self, text: str) -> Set[str]: + """ + Extracts roles from "RoleName:" pattern at line starts. + + Args: + text: Input text to analyze + + Returns: + Set of role names found + """ + roles = set() + pattern = re.compile(self.COLON_PATTERN, re.MULTILINE) + + for match in pattern.finditer(text): + role_name = match.group(1).strip() + if role_name: + roles.add(role_name) + + return roles + + def _extract_bracket_pattern(self, text: str) -> Set[str]: + """ + Extracts roles from "[RoleName]" pattern. + + Args: + text: Input text to analyze + + Returns: + Set of role names found + """ + roles = set() + pattern = re.compile(self.BRACKET_PATTERN) + + for match in pattern.finditer(text): + role_name = match.group(1).strip() + if role_name: + roles.add(role_name) + + return roles diff --git a/backend/test_role_detection.py b/backend/test_role_detection.py new file mode 100644 index 0000000..a569a89 --- /dev/null +++ b/backend/test_role_detection.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +"""Test script for role detection service.""" +import urllib.request +import json + + +def test_role_detection(text, expected_roles, description): + """Test role detection with given text.""" + print(f"\nTesting: {description}") + print(f"Input text: {text[:100]}..." if len(text) > 100 else f"Input text: {text}") + + data = json.dumps({"text": text}).encode('utf-8') + req = urllib.request.Request( + 'http://localhost:8001/api/tts/roles', + data=data, + headers={'Content-Type': 'application/json'} + ) + + try: + response = urllib.request.urlopen(req) + result = json.loads(response.read().decode()) + roles = [role['label'] for role in result['roles']] + + print(f"Detected roles: {roles}") + print(f"Expected roles: {expected_roles}") + + if set(roles) == set(expected_roles): + print("✓ PASS") + return True + else: + print("✗ FAIL: Roles don't match") + return False + except Exception as e: + print(f"✗ FAIL: {str(e)}") + return False + + +def main(): + """Run all role detection tests.""" + print("Testing Role Detection Service...") + + tests = [ + # Test colon pattern + ( + "Alice: Hello there!\nBob: Hi Alice!", + ["Alice", "Bob"], + "Colon pattern extraction" + ), + # Test bracket pattern + ( + "[Narrator] The story begins.\n[Alice] She said hello.", + ["Alice", "Narrator"], + "Bracket pattern extraction" + ), + # Test mixed patterns + ( + "Alice: Hello!\n[Narrator] She waved.\nBob: Hey!", + ["Alice", "Bob", "Narrator"], + "Mixed colon and bracket patterns" + ), + # Test deduplication + ( + "Alice: First line.\nAlice: Second line.\nAlice: Third line.", + ["Alice"], + "Role deduplication" + ), + # Test empty text + ( + "", + ["Narrator"], + "Empty text returns default Narrator" + ), + # Test no role markers + ( + "This is just plain text without any role markers.", + ["Narrator"], + "No role markers returns default Narrator" + ), + # Test whitespace only + ( + " \n\n ", + ["Narrator"], + "Whitespace only returns default Narrator" + ), + ] + + passed = 0 + failed = 0 + + for text, expected, description in tests: + if test_role_detection(text, expected, description): + passed += 1 + else: + failed += 1 + + print(f"\n{'='*50}") + print(f"Results: {passed} passed, {failed} failed") + print(f"{'='*50}") + + return failed == 0 + + +if __name__ == '__main__': + success = main() + exit(0 if success else 1)