Implement role detection service with pattern matching
- Created RoleDetector class with colon and bracket pattern extraction - Implemented role deduplication logic - Added fallback to default 'Narrator' role when no patterns found - Created /api/tts/roles endpoint using RoleDetector - Added comprehensive test suite for role detection - All tests passing (7/7) Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6
This commit is contained in:
@@ -9,6 +9,9 @@ from fastapi.responses import JSONResponse
|
|||||||
from fastapi.exceptions import RequestValidationError
|
from fastapi.exceptions import RequestValidationError
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
# Import services
|
||||||
|
from services.role_detector import RoleDetector
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
@@ -91,9 +94,33 @@ async def general_exception_handler(request: Request, exc: Exception):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Initialize services
|
||||||
|
role_detector = RoleDetector()
|
||||||
|
|
||||||
# API Endpoints
|
# API Endpoints
|
||||||
@app.get("/api/health")
|
@app.get("/api/health")
|
||||||
async def health_check():
|
async def health_check():
|
||||||
"""Health check endpoint."""
|
"""Health check endpoint."""
|
||||||
logger.info("Health check requested")
|
logger.info("Health check requested")
|
||||||
return {"status": "ok"}
|
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
|
||||||
|
|||||||
93
backend/services/role_detector.py
Normal file
93
backend/services/role_detector.py
Normal file
@@ -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
|
||||||
105
backend/test_role_detection.py
Normal file
105
backend/test_role_detection.py
Normal file
@@ -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)
|
||||||
Reference in New Issue
Block a user