Add AI-powered text segmentation for intelligent speaker assignment

- Added segment_text() method to OllamaService for AI-based text splitting
- OllamaService now annotates each text segment with the appropriate speaker
- Updated TTSService to use AI segmentation when available
- Falls back to pattern-based segmentation when Ollama unavailable
- AI intelligently assigns dialogue to speakers and narrative to Narrator
- Added comprehensive test suite for AI segmentation
- Verified AI correctly splits unmarked dialogue between multiple speakers

This makes the tool much more powerful - users can now input plain narrative
text without explicit role markers and the AI will intelligently assign voices
to different characters and the narrator.
This commit is contained in:
2025-12-09 10:59:00 +00:00
parent b48e53ce63
commit d18fddef57
3 changed files with 361 additions and 13 deletions

View File

@@ -1,10 +1,10 @@
"""
Ollama integration service for AI-based speaker detection.
Ollama integration service for AI-based speaker detection and text segmentation.
"""
import json
import logging
import os
from typing import List, Optional
from typing import List, Optional, Dict, Tuple
import httpx
@@ -214,6 +214,145 @@ Return format: {{"speakers": ["Speaker1", "Speaker2", ...]}}
# Look for common patterns like lists or quoted names
return []
async def segment_text(self, text: str, speakers: List[str]) -> List[Tuple[str, str]]:
"""
Uses AI to segment text and assign each segment to a speaker.
Args:
text: Input text to segment
speakers: List of known speakers in the text
Returns:
List of tuples: (speaker_name, text_segment)
Raises:
Exception: If Ollama is unavailable or request fails
"""
try:
async with httpx.AsyncClient(timeout=60.0) as client: # Longer timeout for segmentation
prompt = self._build_segmentation_prompt(text, speakers)
logger.info(f"Requesting AI segmentation for {len(text)} chars with {len(speakers)} speakers")
response = await client.post(
f"{self.base_url}/api/generate",
json={
"model": self.model,
"prompt": prompt,
"stream": False,
"format": "json"
}
)
response.raise_for_status()
result = response.json()
response_text = result.get("response", "")
segments = self._parse_segmentation_response(response_text)
logger.info(f"AI segmentation produced {len(segments)} segments")
return segments
except httpx.TimeoutException:
logger.warning("Ollama segmentation request timed out")
raise Exception("Ollama segmentation timed out")
except httpx.HTTPError as e:
logger.warning(f"Ollama HTTP error during segmentation: {e}")
raise Exception(f"Ollama service error: {e}")
except Exception as e:
logger.warning(f"Ollama segmentation failed: {e}")
raise
def _build_segmentation_prompt(self, text: str, speakers: List[str]) -> str:
"""
Constructs prompt for text segmentation with speaker attribution.
Args:
text: Input text to segment
speakers: List of known speakers
Returns:
Formatted prompt string
"""
speakers_str = ", ".join(f'"{s}"' for s in speakers)
prompt = f"""You are analyzing a story or dialogue. Your task is to split the text into segments and assign each segment to the appropriate speaker.
Known speakers: {speakers_str}
Rules:
1. Split the text into logical segments (sentences or short paragraphs)
2. Assign each segment to the speaker who is speaking or being described
3. Use "Narrator" for narrative text, descriptions, or text that doesn't belong to a specific character
4. If a character is mentioned but not speaking, use "Narrator"
5. Only use speakers from the known speakers list
Return a JSON object with a "segments" array. Each segment should have:
- "speaker": the name of the speaker (must be from the known speakers list or "Narrator")
- "text": the text content for that segment
Example format:
{{
"segments": [
{{"speaker": "Narrator", "text": "The old wizard gazed into his crystal ball."}},
{{"speaker": "Wizard", "text": "I see danger approaching."}},
{{"speaker": "Narrator", "text": "A young knight heard the warning."}}
]
}}
Text to segment:
{text}
Return ONLY the JSON object with the segments array.
"""
return prompt
def _parse_segmentation_response(self, response: str) -> List[Tuple[str, str]]:
"""
Parses Ollama segmentation response to extract speaker-text pairs.
Args:
response: Response text from Ollama
Returns:
List of tuples: (speaker_name, text_content)
"""
if not response or not response.strip():
return []
try:
# Try to parse as JSON
data = json.loads(response.strip())
# Extract segments array
segments_data = []
if isinstance(data, dict):
if "segments" in data:
segments_data = data["segments"]
else:
# Try to find any array in the response
for value in data.values():
if isinstance(value, list):
segments_data = value
break
elif isinstance(data, list):
segments_data = data
# Convert to tuples
segments = []
for seg in segments_data:
if isinstance(seg, dict) and "speaker" in seg and "text" in seg:
speaker = str(seg["speaker"]).strip()
text = str(seg["text"]).strip()
if speaker and text:
segments.append((speaker, text))
return segments
except json.JSONDecodeError:
logger.warning(f"Failed to parse Ollama segmentation response as JSON: {response[:200]}")
return []
async def is_available(self) -> bool:
"""
Checks if Ollama service is available.