Release v1.8.0: Add transcript-fixer skill
## New Skill: transcript-fixer v1.0.0 Correct speech-to-text (ASR/STT) transcription errors through dictionary-based rules and AI-powered corrections with automatic pattern learning. **Features:** - Two-stage correction pipeline (dictionary + AI) - Automatic pattern detection and learning - Domain-specific dictionaries (general, embodied_ai, finance, medical) - SQLite-based correction repository - Team collaboration with import/export - GLM API integration for AI corrections - Cost optimization through dictionary promotion **Use cases:** - Correcting meeting notes, lecture recordings, or interview transcripts - Fixing Chinese/English homophone errors and technical terminology - Building domain-specific correction dictionaries - Improving transcript accuracy through iterative learning **Documentation:** - Complete workflow guides in references/ - SQL query templates - Troubleshooting guide - Team collaboration patterns - API setup instructions **Marketplace updates:** - Updated marketplace to v1.8.0 - Added transcript-fixer plugin (category: productivity) - Updated README.md with skill description and use cases - Updated CLAUDE.md with skill listing and counts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
214
transcript-fixer/scripts/core/ai_processor.py
Normal file
214
transcript-fixer/scripts/core/ai_processor.py
Normal file
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
AI Processor - Stage 2: AI-powered Text Corrections
|
||||
|
||||
SINGLE RESPONSIBILITY: Process text using GLM API for intelligent corrections
|
||||
|
||||
Features:
|
||||
- Split text into chunks for API processing
|
||||
- Call GLM-4.6 for context-aware corrections
|
||||
- Track AI-suggested changes
|
||||
- Handle API errors gracefully
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
from typing import List, Tuple
|
||||
from dataclasses import dataclass
|
||||
import httpx
|
||||
|
||||
|
||||
@dataclass
|
||||
class AIChange:
|
||||
"""Represents an AI-suggested change"""
|
||||
chunk_index: int
|
||||
from_text: str
|
||||
to_text: str
|
||||
confidence: float # 0.0 to 1.0
|
||||
|
||||
|
||||
class AIProcessor:
|
||||
"""
|
||||
Stage 2 Processor: AI-powered corrections using GLM-4.6
|
||||
|
||||
Process:
|
||||
1. Split text into chunks (respecting API limits)
|
||||
2. Send each chunk to GLM API
|
||||
3. Track changes for learning engine
|
||||
4. Preserve formatting and structure
|
||||
"""
|
||||
|
||||
def __init__(self, api_key: str, model: str = "GLM-4.6",
|
||||
base_url: str = "https://open.bigmodel.cn/api/anthropic",
|
||||
fallback_model: str = "GLM-4.5-Air"):
|
||||
"""
|
||||
Initialize AI processor
|
||||
|
||||
Args:
|
||||
api_key: GLM API key
|
||||
model: Model name (default: GLM-4.6)
|
||||
base_url: API base URL
|
||||
fallback_model: Fallback model on primary failure
|
||||
"""
|
||||
self.api_key = api_key
|
||||
self.model = model
|
||||
self.fallback_model = fallback_model
|
||||
self.base_url = base_url
|
||||
self.max_chunk_size = 6000 # Characters per chunk
|
||||
|
||||
def process(self, text: str, context: str = "") -> Tuple[str, List[AIChange]]:
|
||||
"""
|
||||
Process text with AI corrections
|
||||
|
||||
Args:
|
||||
text: Text to correct
|
||||
context: Optional domain/meeting context
|
||||
|
||||
Returns:
|
||||
(corrected_text, list_of_changes)
|
||||
"""
|
||||
chunks = self._split_into_chunks(text)
|
||||
corrected_chunks = []
|
||||
all_changes = []
|
||||
|
||||
print(f"📝 Processing {len(chunks)} chunks with {self.model}...")
|
||||
|
||||
for i, chunk in enumerate(chunks, 1):
|
||||
print(f" Chunk {i}/{len(chunks)}... ", end="", flush=True)
|
||||
|
||||
try:
|
||||
corrected_chunk = self._process_chunk(chunk, context, self.model)
|
||||
corrected_chunks.append(corrected_chunk)
|
||||
|
||||
# TODO: Extract actual changes for learning
|
||||
# For now, we assume the whole chunk changed
|
||||
if corrected_chunk != chunk:
|
||||
all_changes.append(AIChange(
|
||||
chunk_index=i,
|
||||
from_text=chunk[:50] + "...",
|
||||
to_text=corrected_chunk[:50] + "...",
|
||||
confidence=0.9 # Placeholder
|
||||
))
|
||||
|
||||
print("✓")
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ {str(e)[:50]}")
|
||||
|
||||
# Retry with fallback model
|
||||
if self.fallback_model and self.fallback_model != self.model:
|
||||
print(f" Retrying with {self.fallback_model}... ", end="", flush=True)
|
||||
try:
|
||||
corrected_chunk = self._process_chunk(chunk, context, self.fallback_model)
|
||||
corrected_chunks.append(corrected_chunk)
|
||||
print("✓")
|
||||
continue
|
||||
except Exception as e2:
|
||||
print(f"✗ {str(e2)[:50]}")
|
||||
|
||||
print(" Using original text...")
|
||||
corrected_chunks.append(chunk)
|
||||
|
||||
return "\n\n".join(corrected_chunks), all_changes
|
||||
|
||||
def _split_into_chunks(self, text: str) -> List[str]:
|
||||
"""
|
||||
Split text into processable chunks
|
||||
|
||||
Strategy:
|
||||
- Split by double newlines (paragraphs)
|
||||
- Keep chunks under max_chunk_size
|
||||
- Don't split mid-paragraph if possible
|
||||
"""
|
||||
paragraphs = text.split('\n\n')
|
||||
chunks = []
|
||||
current_chunk = []
|
||||
current_length = 0
|
||||
|
||||
for para in paragraphs:
|
||||
para_length = len(para)
|
||||
|
||||
# If single paragraph exceeds limit, force split
|
||||
if para_length > self.max_chunk_size:
|
||||
if current_chunk:
|
||||
chunks.append('\n\n'.join(current_chunk))
|
||||
current_chunk = []
|
||||
current_length = 0
|
||||
|
||||
# Split long paragraph by sentences
|
||||
sentences = re.split(r'([。!?\n])', para)
|
||||
temp_para = ""
|
||||
for i in range(0, len(sentences), 2):
|
||||
sentence = sentences[i] + (sentences[i+1] if i+1 < len(sentences) else "")
|
||||
if len(temp_para) + len(sentence) > self.max_chunk_size:
|
||||
if temp_para:
|
||||
chunks.append(temp_para)
|
||||
temp_para = sentence
|
||||
else:
|
||||
temp_para += sentence
|
||||
if temp_para:
|
||||
chunks.append(temp_para)
|
||||
|
||||
# Normal case: accumulate paragraphs
|
||||
elif current_length + para_length > self.max_chunk_size and current_chunk:
|
||||
chunks.append('\n\n'.join(current_chunk))
|
||||
current_chunk = [para]
|
||||
current_length = para_length
|
||||
else:
|
||||
current_chunk.append(para)
|
||||
current_length += para_length + 2 # +2 for \n\n
|
||||
|
||||
if current_chunk:
|
||||
chunks.append('\n\n'.join(current_chunk))
|
||||
|
||||
return chunks
|
||||
|
||||
def _process_chunk(self, chunk: str, context: str, model: str) -> str:
|
||||
"""Process a single chunk with GLM API"""
|
||||
prompt = self._build_prompt(chunk, context)
|
||||
|
||||
url = f"{self.base_url}/v1/messages"
|
||||
headers = {
|
||||
"anthropic-version": "2023-06-01",
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"content-type": "application/json"
|
||||
}
|
||||
|
||||
data = {
|
||||
"model": model,
|
||||
"max_tokens": 8000,
|
||||
"temperature": 0.3,
|
||||
"messages": [{"role": "user", "content": prompt}]
|
||||
}
|
||||
|
||||
with httpx.Client(timeout=60.0) as client:
|
||||
response = client.post(url, headers=headers, json=data)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
return result["content"][0]["text"]
|
||||
|
||||
def _build_prompt(self, chunk: str, context: str) -> str:
|
||||
"""Build correction prompt for GLM"""
|
||||
base_prompt = """你是专业的会议记录校对专家。请修复以下会议转录中的语音识别错误。
|
||||
|
||||
**修复原则**:
|
||||
1. 严格保留原有格式(时间戳、发言人标识、Markdown标记等)
|
||||
2. 修复明显的同音字错误
|
||||
3. 修复专业术语错误
|
||||
4. 修复语法错误,但保持口语化特征
|
||||
5. 不确定的地方保持原样,不要过度修改
|
||||
|
||||
"""
|
||||
|
||||
if context:
|
||||
base_prompt += f"\n**会议背景**:\n{context}\n"
|
||||
|
||||
base_prompt += f"""
|
||||
**需要修复的内容**:
|
||||
{chunk}
|
||||
|
||||
**请直接输出修复后的文本,不要添加任何解释或标注**:"""
|
||||
|
||||
return base_prompt
|
||||
Reference in New Issue
Block a user