Spaces:
Sleeping
Sleeping
Commit ·
33d3592
0
Parent(s):
Initial commit without node_modules
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +11 -0
- .gitignore +47 -0
- .kiro/specs/ai-questions/design.md +262 -0
- .kiro/specs/ai-questions/requirements.md +91 -0
- .kiro/specs/ai-questions/tasks.md +209 -0
- .kiro/specs/broadcast-export/design.md +274 -0
- .kiro/specs/broadcast-export/requirements.md +87 -0
- .kiro/specs/broadcast-export/tasks.md +194 -0
- .kiro/specs/model-management/design.md +225 -0
- .kiro/specs/model-management/requirements.md +62 -0
- .kiro/specs/model-management/tasks.md +103 -0
- Dockerfile +41 -0
- FIX_GOOGLE_ACCESS.md +27 -0
- GOOGLE_SETUP.md +47 -0
- GOOGLE_SETUP_EASY.md +82 -0
- GOOGLE_SETUP_SIMPLE.md +85 -0
- INTEGRATION_SOLUTION.md +75 -0
- PERFORMANCE_IMPROVEMENTS.md +76 -0
- QUICK_START.md +202 -0
- README.md +221 -0
- README_AR.md +134 -0
- SOLUTION_SUMMARY.md +69 -0
- SUMMARY_FIX_REPORT.md +169 -0
- TECHNICAL_IMPLEMENTATION.md +299 -0
- TROUBLESHOOTING.md +251 -0
- ai_questions.py +773 -0
- app.py +1812 -0
- app_config.py +59 -0
- app_launcher.py +43 -0
- audio_processor.py +391 -0
- check_credentials.py +118 -0
- comprehensive_test.py +225 -0
- credentials.json +16 -0
- custom_components/st-audiorec/.streamlit/config.toml +3 -0
- custom_components/st-audiorec/LICENCE +3 -0
- custom_components/st-audiorec/README.md +3 -0
- custom_components/st-audiorec/demo.py +3 -0
- custom_components/st-audiorec/setup.py +3 -0
- custom_components/st-audiorec/st_audiorec/__init__.py +3 -0
- custom_components/st-audiorec/st_audiorec/frontend/.prettierrc +3 -0
- custom_components/st-audiorec/st_audiorec/frontend/build/asset-manifest.json +3 -0
- custom_components/st-audiorec/st_audiorec/frontend/build/bootstrap.min.css +3 -0
- custom_components/st-audiorec/st_audiorec/frontend/build/index.html +3 -0
- custom_components/st-audiorec/st_audiorec/frontend/build/precache-manifest.30096e2fd9f149157a833e729e772f72.js +3 -0
- custom_components/st-audiorec/st_audiorec/frontend/build/service-worker.js +3 -0
- custom_components/st-audiorec/st_audiorec/frontend/build/static/js/2.ca2bba73.chunk.js +3 -0
- custom_components/st-audiorec/st_audiorec/frontend/build/static/js/2.ca2bba73.chunk.js.LICENSE.txt +3 -0
- custom_components/st-audiorec/st_audiorec/frontend/build/static/js/2.ca2bba73.chunk.js.map +3 -0
- custom_components/st-audiorec/st_audiorec/frontend/build/static/js/main.85742990.chunk.js +3 -0
- custom_components/st-audiorec/st_audiorec/frontend/build/static/js/main.85742990.chunk.js.map +3 -0
.gitattributes
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
custom_components/st-audiorec/** filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.wav filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.ogg filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.tar.gz filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
custom_components/** filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*.pyo
|
| 5 |
+
*.pyd
|
| 6 |
+
*.pyc
|
| 7 |
+
*.log
|
| 8 |
+
*.sqlite3
|
| 9 |
+
*.db
|
| 10 |
+
venv/
|
| 11 |
+
.venv/
|
| 12 |
+
env/
|
| 13 |
+
ENV/
|
| 14 |
+
env.bak/
|
| 15 |
+
pip-wheel-metadata/
|
| 16 |
+
dist/
|
| 17 |
+
*.egg-info/
|
| 18 |
+
|
| 19 |
+
# Editor / OS
|
| 20 |
+
.vscode/
|
| 21 |
+
.idea/
|
| 22 |
+
*.swp
|
| 23 |
+
.DS_Store
|
| 24 |
+
Thumbs.db
|
| 25 |
+
|
| 26 |
+
# Node / frontend
|
| 27 |
+
node_modules/
|
| 28 |
+
npm-debug.log*
|
| 29 |
+
yarn-error.log*
|
| 30 |
+
package-lock.json
|
| 31 |
+
.pnpm-debug.log
|
| 32 |
+
|
| 33 |
+
# Specific frontend inside your component
|
| 34 |
+
custom_components/st-audiorec/st_audiorec/frontend/node_modules/
|
| 35 |
+
|
| 36 |
+
# Virtual env / credentials
|
| 37 |
+
*.env
|
| 38 |
+
.env.*
|
| 39 |
+
|
| 40 |
+
# Hugging Face / caches
|
| 41 |
+
.cache/
|
| 42 |
+
.hf/
|
| 43 |
+
|
| 44 |
+
# IDE metadata
|
| 45 |
+
*.sublime-workspace
|
| 46 |
+
*.sublime-project
|
| 47 |
+
custom_components/st-audiorec/st_audiorec/frontend/node_modules
|
.kiro/specs/ai-questions/design.md
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Design Document
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
The AI Questions feature integrates with the existing SyncMaster broadcast system to provide interactive AI-powered questioning capabilities. Students can select any text segment from broadcast content and engage in natural language conversations with AI to better understand the material. The system leverages the existing Gemini AI infrastructure and maintains the current multilingual support.
|
| 6 |
+
|
| 7 |
+
## Architecture
|
| 8 |
+
|
| 9 |
+
### High-Level Architecture
|
| 10 |
+
|
| 11 |
+
```
|
| 12 |
+
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
| 13 |
+
│ Broadcast UI │ │ Question Engine│ │ AI Service │
|
| 14 |
+
│ │ │ │ │ │
|
| 15 |
+
│ - Text Selection│───▶│ - Context Prep │───▶│ - Gemini AI │
|
| 16 |
+
│ - Ask AI Button │ │ - Question Proc │ │ - Translation │
|
| 17 |
+
│ - Response Area │◀───│ - Response Format│◀───│ - Conversation │
|
| 18 |
+
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
| 19 |
+
│ │ │
|
| 20 |
+
│ │ │
|
| 21 |
+
▼ ▼ ▼
|
| 22 |
+
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
| 23 |
+
│ Session State │ │ Question History│ │ Context Manager │
|
| 24 |
+
│ │ │ │ │ │
|
| 25 |
+
│ - Selected Text │ │ - Q&A Pairs │ │ - Text Context │
|
| 26 |
+
│ - Active Conv │ │ - Timestamps │ │ - Conversation │
|
| 27 |
+
│ - UI Language │ │ - User Prefs │ │ - Memory Mgmt │
|
| 28 |
+
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
### Component Integration
|
| 32 |
+
|
| 33 |
+
The AI Questions feature integrates seamlessly with existing components:
|
| 34 |
+
- **Broadcast System**: Uses existing `broadcast_segments` for text selection
|
| 35 |
+
- **Translation System**: Leverages current `translator.py` for AI responses
|
| 36 |
+
- **UI System**: Extends Streamlit interface with question components
|
| 37 |
+
|
| 38 |
+
## Components and Interfaces
|
| 39 |
+
|
| 40 |
+
### 1. Question Engine (`ai_questions.py`)
|
| 41 |
+
|
| 42 |
+
**Primary Class: `AIQuestionEngine`**
|
| 43 |
+
|
| 44 |
+
```python
|
| 45 |
+
class AIQuestionEngine:
|
| 46 |
+
def __init__(self, translator_instance):
|
| 47 |
+
self.translator = translator_instance
|
| 48 |
+
self.conversation_history = {}
|
| 49 |
+
self.question_templates = {
|
| 50 |
+
'ar': [
|
| 51 |
+
"اشرح هذا النص",
|
| 52 |
+
"أعطني أمثلة على هذا",
|
| 53 |
+
"ما معنى هذا؟",
|
| 54 |
+
"كيف يُستخدم هذا؟",
|
| 55 |
+
"ما أهمية هذا؟"
|
| 56 |
+
],
|
| 57 |
+
'en': [
|
| 58 |
+
"Explain this text",
|
| 59 |
+
"Give me examples of this",
|
| 60 |
+
"What does this mean?",
|
| 61 |
+
"How is this used?",
|
| 62 |
+
"Why is this important?"
|
| 63 |
+
]
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
def process_question(self, selected_text, question, ui_language='ar'):
|
| 67 |
+
"""Process user question about selected text"""
|
| 68 |
+
|
| 69 |
+
def get_question_templates(self, ui_language='ar'):
|
| 70 |
+
"""Get pre-defined question templates"""
|
| 71 |
+
|
| 72 |
+
def format_ai_response(self, response, ui_language='ar'):
|
| 73 |
+
"""Format AI response for display"""
|
| 74 |
+
|
| 75 |
+
def save_conversation(self, text_id, question, answer):
|
| 76 |
+
"""Save Q&A pair to conversation history"""
|
| 77 |
+
|
| 78 |
+
def get_conversation_history(self, text_id):
|
| 79 |
+
"""Retrieve conversation history for specific text"""
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
### 2. UI Components (Extended `app.py`)
|
| 83 |
+
|
| 84 |
+
**Text Selection Interface**
|
| 85 |
+
- Click-to-select functionality for broadcast segments
|
| 86 |
+
- Visual highlighting of selected text
|
| 87 |
+
- Context-aware "Ask AI" button activation
|
| 88 |
+
|
| 89 |
+
**Question Input Modal**
|
| 90 |
+
- Template selection buttons
|
| 91 |
+
- Free-form question input
|
| 92 |
+
- Context display showing selected text
|
| 93 |
+
- Submit and cancel options
|
| 94 |
+
|
| 95 |
+
**Response Display Area**
|
| 96 |
+
- Formatted AI responses
|
| 97 |
+
- Conversation history
|
| 98 |
+
- Copy functionality
|
| 99 |
+
- Follow-up question options
|
| 100 |
+
|
| 101 |
+
### 3. Context Manager
|
| 102 |
+
|
| 103 |
+
**Text Context Preparation**
|
| 104 |
+
```python
|
| 105 |
+
@dataclass
|
| 106 |
+
class QuestionContext:
|
| 107 |
+
selected_text: str
|
| 108 |
+
segment_info: Dict[str, Any] # timestamp, translations, etc.
|
| 109 |
+
conversation_id: str
|
| 110 |
+
ui_language: str
|
| 111 |
+
previous_questions: List[Dict[str, str]]
|
| 112 |
+
```
|
| 113 |
+
|
| 114 |
+
**Conversation Management**
|
| 115 |
+
- Maintains context across multiple questions
|
| 116 |
+
- Manages conversation threads per text segment
|
| 117 |
+
- Handles context window limitations
|
| 118 |
+
- Provides conversation persistence
|
| 119 |
+
|
| 120 |
+
## Data Models
|
| 121 |
+
|
| 122 |
+
### Question Session
|
| 123 |
+
```python
|
| 124 |
+
@dataclass
|
| 125 |
+
class QuestionSession:
|
| 126 |
+
session_id: str
|
| 127 |
+
selected_text: str
|
| 128 |
+
segment_id: str
|
| 129 |
+
start_timestamp: int
|
| 130 |
+
ui_language: str
|
| 131 |
+
conversation: List[QAPair]
|
| 132 |
+
created_at: datetime
|
| 133 |
+
```
|
| 134 |
+
|
| 135 |
+
### Q&A Pair
|
| 136 |
+
```python
|
| 137 |
+
@dataclass
|
| 138 |
+
class QAPair:
|
| 139 |
+
question: str
|
| 140 |
+
answer: str
|
| 141 |
+
timestamp: datetime
|
| 142 |
+
question_type: str # 'template' or 'custom'
|
| 143 |
+
response_time_ms: int
|
| 144 |
+
```
|
| 145 |
+
|
| 146 |
+
### Text Selection
|
| 147 |
+
```python
|
| 148 |
+
@dataclass
|
| 149 |
+
class TextSelection:
|
| 150 |
+
text: str
|
| 151 |
+
segment_id: str
|
| 152 |
+
start_ms: int
|
| 153 |
+
end_ms: int
|
| 154 |
+
translations: Dict[str, str]
|
| 155 |
+
selection_timestamp: int
|
| 156 |
+
```
|
| 157 |
+
|
| 158 |
+
## Error Handling
|
| 159 |
+
|
| 160 |
+
### AI Service Error Handling
|
| 161 |
+
|
| 162 |
+
1. **Service Unavailability**
|
| 163 |
+
- Graceful degradation when Gemini AI is unavailable
|
| 164 |
+
- Clear error messages to users
|
| 165 |
+
- Retry mechanisms for transient failures
|
| 166 |
+
|
| 167 |
+
2. **Response Quality Issues**
|
| 168 |
+
- Validation of AI responses
|
| 169 |
+
- Fallback to simpler question processing
|
| 170 |
+
- User feedback mechanisms for poor responses
|
| 171 |
+
|
| 172 |
+
3. **Context Management Errors**
|
| 173 |
+
- Handling of oversized context windows
|
| 174 |
+
- Conversation history cleanup
|
| 175 |
+
- Memory management for long sessions
|
| 176 |
+
|
| 177 |
+
### User Experience Error Handling
|
| 178 |
+
|
| 179 |
+
```python
|
| 180 |
+
def handle_question_error(self, error_type, context):
|
| 181 |
+
"""Handle various question processing errors"""
|
| 182 |
+
error_messages = {
|
| 183 |
+
'ai_unavailable': {
|
| 184 |
+
'ar': 'خدمة الذكاء الاصطناعي غير متاحة حالياً. يرجى المحاولة لاحقاً.',
|
| 185 |
+
'en': 'AI service is currently unavailable. Please try again later.'
|
| 186 |
+
},
|
| 187 |
+
'invalid_selection': {
|
| 188 |
+
'ar': 'يرجى تحديد نص صالح قبل طرح السؤال.',
|
| 189 |
+
'en': 'Please select valid text before asking a question.'
|
| 190 |
+
},
|
| 191 |
+
'processing_timeout': {
|
| 192 |
+
'ar': 'انتهت مهلة معالجة السؤال. يرجى المحاولة مرة أخرى.',
|
| 193 |
+
'en': 'Question processing timed out. Please try again.'
|
| 194 |
+
}
|
| 195 |
+
}
|
| 196 |
+
```
|
| 197 |
+
|
| 198 |
+
## Testing Strategy
|
| 199 |
+
|
| 200 |
+
### Unit Testing
|
| 201 |
+
- **Question Processing Tests**: Various question types and text selections
|
| 202 |
+
- **Context Management Tests**: Conversation history and memory management
|
| 203 |
+
- **AI Integration Tests**: Response formatting and error handling
|
| 204 |
+
- **UI Component Tests**: Text selection and modal interactions
|
| 205 |
+
|
| 206 |
+
### Integration Testing
|
| 207 |
+
- **End-to-End Question Flow**: From text selection to AI response display
|
| 208 |
+
- **Multi-language Testing**: Arabic and English question processing
|
| 209 |
+
- **Conversation Continuity**: Follow-up questions and context maintenance
|
| 210 |
+
- **Performance Testing**: Response times and memory usage
|
| 211 |
+
|
| 212 |
+
### User Acceptance Testing
|
| 213 |
+
- **Student Workflow Testing**: Real-world usage scenarios
|
| 214 |
+
- **Question Quality Testing**: Relevance and accuracy of AI responses
|
| 215 |
+
- **Interface Usability Testing**: Ease of text selection and question input
|
| 216 |
+
- **Accessibility Testing**: Screen reader compatibility and keyboard navigation
|
| 217 |
+
|
| 218 |
+
## Implementation Phases
|
| 219 |
+
|
| 220 |
+
### Phase 1: Core Question Engine
|
| 221 |
+
- Implement `AIQuestionEngine` class
|
| 222 |
+
- Add basic question processing with Gemini AI
|
| 223 |
+
- Create question templates for both languages
|
| 224 |
+
- Integrate with existing translator system
|
| 225 |
+
|
| 226 |
+
### Phase 2: UI Integration
|
| 227 |
+
- Add text selection functionality to broadcast segments
|
| 228 |
+
- Implement "Ask AI" button and modal interface
|
| 229 |
+
- Create question input and response display components
|
| 230 |
+
- Add visual feedback for text selection
|
| 231 |
+
|
| 232 |
+
### Phase 3: Conversation Management
|
| 233 |
+
- Implement conversation history tracking
|
| 234 |
+
- Add follow-up question capabilities
|
| 235 |
+
- Create context management for multi-turn conversations
|
| 236 |
+
- Add conversation persistence across sessions
|
| 237 |
+
|
| 238 |
+
### Phase 4: Advanced Features
|
| 239 |
+
- Add copy/export functionality for Q&A pairs
|
| 240 |
+
- Implement conversation search and filtering
|
| 241 |
+
- Add question analytics and usage tracking
|
| 242 |
+
- Create advanced question templates and suggestions
|
| 243 |
+
|
| 244 |
+
## Performance Considerations
|
| 245 |
+
|
| 246 |
+
### Response Time Optimization
|
| 247 |
+
- Asynchronous AI request processing
|
| 248 |
+
- Response caching for common questions
|
| 249 |
+
- Progressive loading for long conversations
|
| 250 |
+
- Optimized context preparation
|
| 251 |
+
|
| 252 |
+
### Memory Management
|
| 253 |
+
- Efficient conversation history storage
|
| 254 |
+
- Automatic cleanup of old conversations
|
| 255 |
+
- Streaming responses for long AI answers
|
| 256 |
+
- Optimized text selection handling
|
| 257 |
+
|
| 258 |
+
### Scalability
|
| 259 |
+
- Support for multiple concurrent question sessions
|
| 260 |
+
- Efficient handling of large broadcast segments
|
| 261 |
+
- Configurable conversation history limits
|
| 262 |
+
- Resource usage monitoring and optimization
|
.kiro/specs/ai-questions/requirements.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Requirements Document
|
| 2 |
+
|
| 3 |
+
## Introduction
|
| 4 |
+
|
| 5 |
+
This feature adds AI-powered question generation functionality to the SyncMaster broadcast system. Students can select any text segment from the broadcast and ask the AI to generate questions, explanations, or provide additional information about specific content. This enhances the learning experience by allowing interactive exploration of lecture content through intelligent questioning.
|
| 6 |
+
|
| 7 |
+
## Requirements
|
| 8 |
+
|
| 9 |
+
### Requirement 1
|
| 10 |
+
|
| 11 |
+
**User Story:** As a student reviewing broadcast content, I want to select any text segment and ask AI questions about it, so that I can better understand specific concepts or get clarification on unclear points.
|
| 12 |
+
|
| 13 |
+
#### Acceptance Criteria
|
| 14 |
+
|
| 15 |
+
1. WHEN the user clicks on any broadcast segment THEN the system SHALL highlight the selected text
|
| 16 |
+
2. WHEN text is selected THEN the system SHALL display a "❓ Ask AI" button
|
| 17 |
+
3. WHEN the user clicks "Ask AI" THEN the system SHALL open a question input interface
|
| 18 |
+
4. WHEN the user submits a question THEN the system SHALL send the selected text and question to the AI service
|
| 19 |
+
5. IF no text is selected THEN the "Ask AI" button SHALL be disabled with explanatory tooltip
|
| 20 |
+
|
| 21 |
+
### Requirement 2
|
| 22 |
+
|
| 23 |
+
**User Story:** As a student, I want to ask different types of questions about the selected content, so that I can get explanations, examples, or deeper insights about the material.
|
| 24 |
+
|
| 25 |
+
#### Acceptance Criteria
|
| 26 |
+
|
| 27 |
+
1. WHEN the question interface opens THEN the system SHALL provide quick question templates
|
| 28 |
+
2. WHEN templates are provided THEN they SHALL include: "Explain this", "Give examples", "What does this mean?", "How is this used?"
|
| 29 |
+
3. WHEN the user selects a template THEN the system SHALL auto-fill the question input
|
| 30 |
+
4. WHEN the user types a custom question THEN the system SHALL accept free-form text input
|
| 31 |
+
5. WHEN processing the question THEN the system SHALL include the selected text as context for the AI
|
| 32 |
+
|
| 33 |
+
### Requirement 3
|
| 34 |
+
|
| 35 |
+
**User Story:** As a student, I want to receive AI-generated answers in my preferred language, so that I can understand the explanations clearly.
|
| 36 |
+
|
| 37 |
+
#### Acceptance Criteria
|
| 38 |
+
|
| 39 |
+
1. WHEN generating AI responses THEN the system SHALL use the current UI language setting
|
| 40 |
+
2. WHEN the UI is in Arabic THEN AI responses SHALL be in Arabic
|
| 41 |
+
3. WHEN the UI is in English THEN AI responses SHALL be in English
|
| 42 |
+
4. WHEN the selected text is in a different language THEN the AI SHALL provide context-aware responses
|
| 43 |
+
5. IF language detection fails THEN the system SHALL default to the UI language
|
| 44 |
+
|
| 45 |
+
### Requirement 4
|
| 46 |
+
|
| 47 |
+
**User Story:** As a student, I want to see the AI's answer clearly formatted and easy to read, so that I can quickly understand the information provided.
|
| 48 |
+
|
| 49 |
+
#### Acceptance Criteria
|
| 50 |
+
|
| 51 |
+
1. WHEN the AI responds THEN the answer SHALL be displayed in a dedicated response area
|
| 52 |
+
2. WHEN displaying the response THEN the system SHALL show the original selected text for reference
|
| 53 |
+
3. WHEN formatting the response THEN the system SHALL use clear typography and spacing
|
| 54 |
+
4. WHEN the response is long THEN the system SHALL provide scrollable content area
|
| 55 |
+
5. WHEN multiple questions are asked THEN the system SHALL maintain a conversation history
|
| 56 |
+
|
| 57 |
+
### Requirement 5
|
| 58 |
+
|
| 59 |
+
**User Story:** As a student, I want to ask follow-up questions about the same content, so that I can have a natural conversation with the AI about the topic.
|
| 60 |
+
|
| 61 |
+
#### Acceptance Criteria
|
| 62 |
+
|
| 63 |
+
1. WHEN an AI response is displayed THEN the system SHALL provide an option to ask follow-up questions
|
| 64 |
+
2. WHEN asking follow-up questions THEN the system SHALL maintain context from previous questions
|
| 65 |
+
3. WHEN the conversation continues THEN the system SHALL display the full conversation thread
|
| 66 |
+
4. WHEN starting a new question on different content THEN the system SHALL start a fresh conversation
|
| 67 |
+
5. IF the conversation becomes too long THEN the system SHALL provide option to clear history
|
| 68 |
+
|
| 69 |
+
### Requirement 6
|
| 70 |
+
|
| 71 |
+
**User Story:** As a student, I want to copy or save the AI's answers, so that I can include them in my notes or study materials.
|
| 72 |
+
|
| 73 |
+
#### Acceptance Criteria
|
| 74 |
+
|
| 75 |
+
1. WHEN an AI response is displayed THEN the system SHALL provide a "Copy" button
|
| 76 |
+
2. WHEN the user clicks "Copy" THEN the response text SHALL be copied to clipboard
|
| 77 |
+
3. WHEN copying THEN the system SHALL include both the original question and AI answer
|
| 78 |
+
4. WHEN multiple Q&A pairs exist THEN the user SHALL be able to copy individual answers or the entire conversation
|
| 79 |
+
5. WHEN copying THEN the system SHALL format the text appropriately for pasting into documents
|
| 80 |
+
|
| 81 |
+
### Requirement 7
|
| 82 |
+
|
| 83 |
+
**User Story:** As a student, I want the AI question feature to work seamlessly with the existing broadcast interface, so that my workflow is not disrupted.
|
| 84 |
+
|
| 85 |
+
#### Acceptance Criteria
|
| 86 |
+
|
| 87 |
+
1. WHEN the question interface is open THEN the broadcast content SHALL remain visible
|
| 88 |
+
2. WHEN asking questions THEN the broadcast playback SHALL not be interrupted
|
| 89 |
+
3. WHEN switching between segments THEN any open question interface SHALL adapt to the new selection
|
| 90 |
+
4. WHEN the broadcast is updated with new segments THEN the question feature SHALL work with new content
|
| 91 |
+
5. IF the AI service is unavailable THEN the system SHALL display appropriate error messages and fallback options
|
.kiro/specs/ai-questions/tasks.md
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Implementation Plan
|
| 2 |
+
|
| 3 |
+
- [x] 1. Set up AI question engine infrastructure
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
- Create ai_questions.py module with AIQuestionEngine class
|
| 7 |
+
- Implement basic question processing using existing Gemini AI integration
|
| 8 |
+
- Add question templates for Arabic and English languages
|
| 9 |
+
- Create data models for question sessions and Q&A pairs
|
| 10 |
+
- _Requirements: 1.1, 2.1, 3.1_
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
- [ ] 2. Implement text selection functionality
|
| 15 |
+
- [ ] 2.1 Add clickable text selection to broadcast segments
|
| 16 |
+
- Modify broadcast segment display to make text selectable
|
| 17 |
+
- Implement visual highlighting for selected text
|
| 18 |
+
- Add session state management for selected text
|
| 19 |
+
- Create text selection validation and error handling
|
| 20 |
+
|
| 21 |
+
- Write unit tests for text selection functionality
|
| 22 |
+
- _Requirements: 1.1, 1.5_
|
| 23 |
+
|
| 24 |
+
- [ ] 2.2 Create "Ask AI" button with conditional display
|
| 25 |
+
- Add "Ask AI" button that appears when text is selected
|
| 26 |
+
- Implement button state management (enabled/disabled)
|
| 27 |
+
- Add tooltips and help text for button functionality
|
| 28 |
+
- Create button styling consistent with existing UI
|
| 29 |
+
|
| 30 |
+
- Test button behavior with different text selections
|
| 31 |
+
- _Requirements: 1.2, 1.5_
|
| 32 |
+
|
| 33 |
+
- [ ] 3. Build question input interface
|
| 34 |
+
- [ ] 3.1 Create question input modal with templates
|
| 35 |
+
- Implement modal dialog for question input
|
| 36 |
+
- Add pre-defined question templates with quick selection
|
| 37 |
+
|
| 38 |
+
- Create free-form text input for custom questions
|
| 39 |
+
- Display selected text context in the modal
|
| 40 |
+
- Add submit and cancel functionality
|
| 41 |
+
- _Requirements: 2.1, 2.2, 2.3, 2.4_
|
| 42 |
+
|
| 43 |
+
- [ ] 3.2 Implement question processing and AI integration
|
| 44 |
+
- Connect question input to AIQuestionEngine
|
| 45 |
+
- Add context preparation including selected text and metadata
|
| 46 |
+
|
| 47 |
+
- Implement AI request processing with error handling
|
| 48 |
+
- Add loading indicators during AI processing
|
| 49 |
+
- Create timeout handling for long AI responses
|
| 50 |
+
- _Requirements: 1.4, 2.5, 7.5_
|
| 51 |
+
|
| 52 |
+
- [ ] 4. Create AI response display system
|
| 53 |
+
- [x] 4.1 Build response display area with formatting
|
| 54 |
+
|
| 55 |
+
- Create dedicated area for displaying AI responses
|
| 56 |
+
- Implement proper text formatting and typography
|
| 57 |
+
- Add scrollable content area for long responses
|
| 58 |
+
- Display original selected text for reference
|
| 59 |
+
- Create responsive design for different screen sizes
|
| 60 |
+
- _Requirements: 4.1, 4.2, 4.3, 4.4_
|
| 61 |
+
|
| 62 |
+
- [x] 4.2 Add conversation history management
|
| 63 |
+
|
| 64 |
+
- Implement conversation thread display
|
| 65 |
+
- Add conversation history storage in session state
|
| 66 |
+
- Create conversation navigation and scrolling
|
| 67 |
+
- Add conversation clearing functionality
|
| 68 |
+
- Test conversation persistence across UI interactions
|
| 69 |
+
- _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5_
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
- [ ] 5. Implement multilingual support
|
| 73 |
+
- [ ] 5.1 Add language-aware AI response generation
|
| 74 |
+
- Configure AI responses based on UI language setting
|
| 75 |
+
- Implement language detection for selected text
|
| 76 |
+
- Add context-aware response generation
|
| 77 |
+
- Create fallback mechanisms for language detection failures
|
| 78 |
+
- Test multilingual question processing
|
| 79 |
+
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5_
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
- [ ] 5.2 Create multilingual question templates
|
| 83 |
+
- Implement Arabic question templates
|
| 84 |
+
- Add English question templates
|
| 85 |
+
- Create template selection based on UI language
|
| 86 |
+
- Add template customization and expansion
|
| 87 |
+
- Test template functionality in both languages
|
| 88 |
+
- _Requirements: 2.1, 2.2, 2.3_
|
| 89 |
+
|
| 90 |
+
- [ ] 6. Add copy and export functionality
|
| 91 |
+
- [ ] 6.1 Implement response copying features
|
| 92 |
+
- Add "Copy" button for individual AI responses
|
| 93 |
+
- Implement clipboard integration for copying text
|
| 94 |
+
- Create formatted copying including questions and answers
|
| 95 |
+
- Add copy confirmation feedback to users
|
| 96 |
+
- Test copying functionality across different browsers
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
- _Requirements: 6.1, 6.2, 6.3_
|
| 100 |
+
|
| 101 |
+
- [ ] 6.2 Create conversation export capabilities
|
| 102 |
+
- Add functionality to copy entire conversations
|
| 103 |
+
- Implement formatted export for study materials
|
| 104 |
+
- Create export options for individual Q&A pairs
|
| 105 |
+
- Add export formatting for different use cases
|
| 106 |
+
- Test export functionality with various conversation lengths
|
| 107 |
+
- _Requirements: 6.4, 6.5_
|
| 108 |
+
|
| 109 |
+
- [ ] 7. Integrate with existing broadcast system
|
| 110 |
+
- [ ] 7.1 Ensure seamless broadcast integration
|
| 111 |
+
- Integrate question interface with existing broadcast UI
|
| 112 |
+
- Maintain broadcast visibility during question sessions
|
| 113 |
+
- Ensure broadcast playback is not interrupted by questions
|
| 114 |
+
- Add smooth transitions between broadcast and question modes
|
| 115 |
+
- Test integration with all existing broadcast features
|
| 116 |
+
- _Requirements: 7.1, 7.2_
|
| 117 |
+
|
| 118 |
+
- [ ] 7.2 Handle dynamic broadcast updates
|
| 119 |
+
- Adapt question interface to new broadcast segments
|
| 120 |
+
- Update text selection when broadcast content changes
|
| 121 |
+
- Maintain question sessions across broadcast updates
|
| 122 |
+
- Handle segment deletion and modification gracefully
|
| 123 |
+
- Test with real-time broadcast updates
|
| 124 |
+
- _Requirements: 7.3, 7.4_
|
| 125 |
+
|
| 126 |
+
- [ ] 8. Implement error handling and fallbacks
|
| 127 |
+
- [ ] 8.1 Add comprehensive error handling
|
| 128 |
+
- Implement AI service unavailability handling
|
| 129 |
+
- Add user-friendly error messages in both languages
|
| 130 |
+
- Create retry mechanisms for failed AI requests
|
| 131 |
+
- Add fallback options when AI service fails
|
| 132 |
+
- Test error scenarios and recovery mechanisms
|
| 133 |
+
- _Requirements: 7.5_
|
| 134 |
+
|
| 135 |
+
- [ ] 8.2 Create robust conversation management
|
| 136 |
+
- Add conversation cleanup for memory management
|
| 137 |
+
- Implement conversation size limits and warnings
|
| 138 |
+
- Create automatic conversation archiving
|
| 139 |
+
- Add conversation recovery after errors
|
| 140 |
+
- Test conversation stability under various conditions
|
| 141 |
+
- _Requirements: 5.5_
|
| 142 |
+
|
| 143 |
+
- [ ] 9. Add advanced question features
|
| 144 |
+
- [ ] 9.1 Implement follow-up question capabilities
|
| 145 |
+
- Add "Ask follow-up" functionality to responses
|
| 146 |
+
- Maintain conversation context across multiple questions
|
| 147 |
+
- Create intelligent context summarization for long conversations
|
| 148 |
+
- Add conversation branching for different topics
|
| 149 |
+
- Test follow-up question accuracy and relevance
|
| 150 |
+
- _Requirements: 5.1, 5.2, 5.3_
|
| 151 |
+
|
| 152 |
+
- [ ] 9.2 Create question suggestion system
|
| 153 |
+
- Implement AI-powered question suggestions based on selected text
|
| 154 |
+
- Add smart question recommendations
|
| 155 |
+
- Create question difficulty levels (basic, intermediate, advanced)
|
| 156 |
+
- Add question categorization (explanation, examples, application)
|
| 157 |
+
- Test suggestion quality and relevance
|
| 158 |
+
- _Requirements: 2.1, 2.2_
|
| 159 |
+
|
| 160 |
+
- [ ] 10. Optimize performance and user experience
|
| 161 |
+
- [ ] 10.1 Implement performance optimizations
|
| 162 |
+
- Add asynchronous processing for AI requests
|
| 163 |
+
- Implement response caching for common questions
|
| 164 |
+
- Create progressive loading for long conversations
|
| 165 |
+
- Add memory optimization for conversation history
|
| 166 |
+
- Test performance with large broadcast segments
|
| 167 |
+
- _Requirements: 4.4, 5.5_
|
| 168 |
+
|
| 169 |
+
- [ ] 10.2 Enhance user experience features
|
| 170 |
+
- Add keyboard shortcuts for common actions
|
| 171 |
+
- Implement drag-and-drop text selection
|
| 172 |
+
- Create question history search functionality
|
| 173 |
+
- Add question bookmarking and favorites
|
| 174 |
+
- Test accessibility features and screen reader compatibility
|
| 175 |
+
- _Requirements: 7.1, 7.2_
|
| 176 |
+
|
| 177 |
+
- [ ] 11. Create comprehensive testing suite
|
| 178 |
+
- [ ] 11.1 Write unit tests for question engine
|
| 179 |
+
- Create tests for AIQuestionEngine class methods
|
| 180 |
+
- Add tests for question processing and formatting
|
| 181 |
+
- Write tests for conversation management
|
| 182 |
+
- Create tests for error handling scenarios
|
| 183 |
+
- Implement test data fixtures for various question types
|
| 184 |
+
- _Requirements: All requirements validation_
|
| 185 |
+
|
| 186 |
+
- [ ] 11.2 Implement integration tests
|
| 187 |
+
- Create end-to-end tests for complete question flow
|
| 188 |
+
- Add tests for multilingual question processing
|
| 189 |
+
- Write tests for UI component interactions
|
| 190 |
+
- Create tests for broadcast system integration
|
| 191 |
+
- Implement performance tests for AI response times
|
| 192 |
+
- _Requirements: All requirements validation_
|
| 193 |
+
|
| 194 |
+
- [ ] 12. Final integration and polish
|
| 195 |
+
- [ ] 12.1 Complete system integration
|
| 196 |
+
- Ensure seamless integration with all existing features
|
| 197 |
+
- Test compatibility with export functionality
|
| 198 |
+
- Verify proper session state management
|
| 199 |
+
- Add configuration options for question features
|
| 200 |
+
- Create deployment-ready code with proper error handling
|
| 201 |
+
- _Requirements: All requirements_
|
| 202 |
+
|
| 203 |
+
- [ ] 12.2 Create user documentation and help
|
| 204 |
+
- Write user guide for AI question features
|
| 205 |
+
- Create in-app help and tooltips
|
| 206 |
+
- Add troubleshooting documentation
|
| 207 |
+
- Create video tutorials for complex workflows
|
| 208 |
+
- Document keyboard shortcuts and advanced features
|
| 209 |
+
- _Requirements: 7.5_
|
.kiro/specs/broadcast-export/design.md
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Design Document
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
The broadcast export feature extends SyncMaster Enhanced with the ability to export lecture content from a specific timestamp. The system will integrate seamlessly with the existing broadcast functionality, providing students with formatted documents containing original text, translations, and AI-generated summaries. The design prioritizes simplicity, reliability, and multilingual support.
|
| 6 |
+
|
| 7 |
+
## Architecture
|
| 8 |
+
|
| 9 |
+
### High-Level Architecture
|
| 10 |
+
|
| 11 |
+
```
|
| 12 |
+
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
| 13 |
+
│ Streamlit UI │ │ Export Engine │ │ File Generators│
|
| 14 |
+
│ │ │ │ │ │
|
| 15 |
+
│ - Export Button │───▶│ - Data Filter │───▶│ - Word Exporter │
|
| 16 |
+
│ - Preview Modal │ │ - Content Prep │ │ - GDocs Exporter│
|
| 17 |
+
│ - Download Link │◀───│ - Format Router │◀───│ - Summary Gen │
|
| 18 |
+
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
| 19 |
+
│ │ │
|
| 20 |
+
│ │ │
|
| 21 |
+
▼ ▼ ▼
|
| 22 |
+
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
| 23 |
+
│ Session State │ │ Broadcast Data │ │ External APIs │
|
| 24 |
+
│ │ │ │ │ │
|
| 25 |
+
│ - Export Time │ │ - Segments │ │ - Google Docs │
|
| 26 |
+
│ - UI Language │ │ - Translations │ │ - Gemini AI │
|
| 27 |
+
│ - Export Config │ │ - Timestamps │ │ - File System │
|
| 28 |
+
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
### Component Integration
|
| 32 |
+
|
| 33 |
+
The export feature integrates with existing SyncMaster components:
|
| 34 |
+
- **Broadcast System**: Uses `st.session_state.broadcast_segments` for data
|
| 35 |
+
- **Translation System**: Leverages existing `translator.py` for summaries
|
| 36 |
+
- **UI System**: Extends current Streamlit interface with export controls
|
| 37 |
+
|
| 38 |
+
## Components and Interfaces
|
| 39 |
+
|
| 40 |
+
### 1. Export Engine (`exporter.py`)
|
| 41 |
+
|
| 42 |
+
**Primary Class: `BroadcastExporter`**
|
| 43 |
+
|
| 44 |
+
```python
|
| 45 |
+
class BroadcastExporter:
|
| 46 |
+
def __init__(self, translator_instance):
|
| 47 |
+
self.translator = translator_instance
|
| 48 |
+
self.supported_formats = ['word', 'google_docs']
|
| 49 |
+
|
| 50 |
+
def filter_segments_from_timestamp(self, segments, export_timestamp):
|
| 51 |
+
"""Filter broadcast segments from export timestamp"""
|
| 52 |
+
|
| 53 |
+
def prepare_export_content(self, segments, include_summary=True):
|
| 54 |
+
"""Prepare structured content for export"""
|
| 55 |
+
|
| 56 |
+
def export_to_word(self, content, filename):
|
| 57 |
+
"""Generate Word document"""
|
| 58 |
+
|
| 59 |
+
def export_to_google_docs(self, content, title):
|
| 60 |
+
"""Create Google Docs document"""
|
| 61 |
+
|
| 62 |
+
def generate_export_summary(self, segments, target_language='ar'):
|
| 63 |
+
"""Generate summary for export content"""
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
### 2. UI Components (Extended `app.py`)
|
| 67 |
+
|
| 68 |
+
**Export Button Integration**
|
| 69 |
+
- Location: Within the broadcast expander section
|
| 70 |
+
- Trigger: Records current timestamp and opens export modal
|
| 71 |
+
- State Management: Uses session state for export configuration
|
| 72 |
+
|
| 73 |
+
**Export Modal**
|
| 74 |
+
- Preview content display
|
| 75 |
+
- Format selection (Word/Google Docs)
|
| 76 |
+
- Export confirmation/cancellation
|
| 77 |
+
- Progress indication during export
|
| 78 |
+
|
| 79 |
+
### 3. Document Generators
|
| 80 |
+
|
| 81 |
+
**Word Document Structure:**
|
| 82 |
+
```
|
| 83 |
+
Title: محاضرة - [Date/Time]
|
| 84 |
+
Export Time: [Timestamp]
|
| 85 |
+
═══════════════════════════════════
|
| 86 |
+
|
| 87 |
+
📻 البرودكاست المُصدر
|
| 88 |
+
═══════════════════════════════════
|
| 89 |
+
|
| 90 |
+
[Segment 1: Time Range]
|
| 91 |
+
Original: [Text]
|
| 92 |
+
Translation: [Text]
|
| 93 |
+
|
| 94 |
+
[Segment 2: Time Range]
|
| 95 |
+
Original: [Text]
|
| 96 |
+
Translation: [Text]
|
| 97 |
+
|
| 98 |
+
📝 الملخص
|
| 99 |
+
═══════════════════════════════════
|
| 100 |
+
[AI-Generated Summary]
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
**Google Docs Integration:**
|
| 104 |
+
- Uses Google Docs API v1
|
| 105 |
+
- Creates shareable documents
|
| 106 |
+
- Applies consistent formatting
|
| 107 |
+
- Handles authentication via service account
|
| 108 |
+
|
| 109 |
+
## Data Models
|
| 110 |
+
|
| 111 |
+
### Export Configuration
|
| 112 |
+
```python
|
| 113 |
+
@dataclass
|
| 114 |
+
class ExportConfig:
|
| 115 |
+
export_timestamp: int # Unix timestamp in milliseconds
|
| 116 |
+
format_type: str # 'word' or 'google_docs'
|
| 117 |
+
include_summary: bool # Whether to include AI summary
|
| 118 |
+
ui_language: str # 'ar' or 'en' for interface
|
| 119 |
+
target_language: str # Translation language for summary
|
| 120 |
+
```
|
| 121 |
+
|
| 122 |
+
### Export Content
|
| 123 |
+
```python
|
| 124 |
+
@dataclass
|
| 125 |
+
class ExportContent:
|
| 126 |
+
title: str
|
| 127 |
+
export_time: str
|
| 128 |
+
segments: List[BroadcastSegment]
|
| 129 |
+
summary: Optional[str]
|
| 130 |
+
metadata: Dict[str, Any]
|
| 131 |
+
```
|
| 132 |
+
|
| 133 |
+
### Broadcast Segment (Extended)
|
| 134 |
+
```python
|
| 135 |
+
@dataclass
|
| 136 |
+
class BroadcastSegment:
|
| 137 |
+
id: str
|
| 138 |
+
start_ms: int
|
| 139 |
+
end_ms: int
|
| 140 |
+
text: str
|
| 141 |
+
translations: Dict[str, str] # language_code -> translated_text
|
| 142 |
+
timestamp_formatted: str # Human-readable time range
|
| 143 |
+
```
|
| 144 |
+
|
| 145 |
+
## Error Handling
|
| 146 |
+
|
| 147 |
+
### Export Process Error Handling
|
| 148 |
+
|
| 149 |
+
1. **Timestamp Validation**
|
| 150 |
+
- Verify export timestamp is valid
|
| 151 |
+
- Handle edge cases (no segments after timestamp)
|
| 152 |
+
- Provide user feedback for empty exports
|
| 153 |
+
|
| 154 |
+
2. **Document Generation Errors**
|
| 155 |
+
- Word document creation failures
|
| 156 |
+
- Google Docs API errors
|
| 157 |
+
- File system permission issues
|
| 158 |
+
- Network connectivity problems
|
| 159 |
+
|
| 160 |
+
3. **Summary Generation Errors**
|
| 161 |
+
- AI service unavailability
|
| 162 |
+
- Empty content handling
|
| 163 |
+
- Fallback to export without summary
|
| 164 |
+
|
| 165 |
+
4. **User Experience Error Handling**
|
| 166 |
+
- Clear error messages in user's language
|
| 167 |
+
- Graceful degradation (Word fallback for Google Docs)
|
| 168 |
+
- Retry mechanisms for transient failures
|
| 169 |
+
|
| 170 |
+
### Error Recovery Strategies
|
| 171 |
+
|
| 172 |
+
```python
|
| 173 |
+
def export_with_fallback(self, content, format_type):
|
| 174 |
+
"""Export with automatic fallback handling"""
|
| 175 |
+
try:
|
| 176 |
+
if format_type == 'google_docs':
|
| 177 |
+
return self.export_to_google_docs(content)
|
| 178 |
+
except GoogleDocsError:
|
| 179 |
+
# Fallback to Word export
|
| 180 |
+
return self.export_to_word(content)
|
| 181 |
+
except Exception as e:
|
| 182 |
+
# Log error and provide user feedback
|
| 183 |
+
return self.handle_export_error(e)
|
| 184 |
+
```
|
| 185 |
+
|
| 186 |
+
## Testing Strategy
|
| 187 |
+
|
| 188 |
+
### Unit Testing
|
| 189 |
+
- **Export Engine Tests**: Data filtering, content preparation, format generation
|
| 190 |
+
- **Document Generator Tests**: Word document structure, Google Docs API integration
|
| 191 |
+
- **Error Handling Tests**: Various failure scenarios and recovery mechanisms
|
| 192 |
+
|
| 193 |
+
### Integration Testing
|
| 194 |
+
- **End-to-End Export Flow**: From button click to document download
|
| 195 |
+
- **Multi-language Testing**: Arabic and English interface testing
|
| 196 |
+
- **Cross-format Testing**: Consistency between Word and Google Docs exports
|
| 197 |
+
|
| 198 |
+
### User Acceptance Testing
|
| 199 |
+
- **Student Workflow Testing**: Real lecture scenario testing
|
| 200 |
+
- **Performance Testing**: Export speed with large broadcast segments
|
| 201 |
+
- **Accessibility Testing**: Screen reader compatibility, keyboard navigation
|
| 202 |
+
|
| 203 |
+
### Test Data Scenarios
|
| 204 |
+
```python
|
| 205 |
+
# Test scenarios for broadcast segments
|
| 206 |
+
test_scenarios = [
|
| 207 |
+
"empty_broadcast", # No segments to export
|
| 208 |
+
"single_segment", # One segment after timestamp
|
| 209 |
+
"multiple_segments", # Multiple segments with translations
|
| 210 |
+
"mixed_languages", # Segments in different languages
|
| 211 |
+
"large_content", # Performance testing with many segments
|
| 212 |
+
"special_characters", # Unicode and RTL text handling
|
| 213 |
+
]
|
| 214 |
+
```
|
| 215 |
+
|
| 216 |
+
## Implementation Phases
|
| 217 |
+
|
| 218 |
+
### Phase 1: Core Export Engine
|
| 219 |
+
- Implement `BroadcastExporter` class
|
| 220 |
+
- Add timestamp filtering functionality
|
| 221 |
+
- Create basic Word document generation
|
| 222 |
+
- Integrate with existing broadcast data
|
| 223 |
+
|
| 224 |
+
### Phase 2: UI Integration
|
| 225 |
+
- Add export button to broadcast section
|
| 226 |
+
- Implement export modal with preview
|
| 227 |
+
- Add progress indicators and user feedback
|
| 228 |
+
- Handle multilingual UI elements
|
| 229 |
+
|
| 230 |
+
### Phase 3: Advanced Features
|
| 231 |
+
- Google Docs integration
|
| 232 |
+
- Enhanced document formatting
|
| 233 |
+
- Summary generation for export content
|
| 234 |
+
- Error handling and fallback mechanisms
|
| 235 |
+
|
| 236 |
+
### Phase 4: Testing and Optimization
|
| 237 |
+
- Comprehensive testing suite
|
| 238 |
+
- Performance optimization
|
| 239 |
+
- User experience refinements
|
| 240 |
+
- Documentation and help content
|
| 241 |
+
|
| 242 |
+
## Security Considerations
|
| 243 |
+
|
| 244 |
+
### Data Privacy
|
| 245 |
+
- Export content remains on user's device or chosen cloud service
|
| 246 |
+
- No intermediate storage of sensitive lecture content
|
| 247 |
+
- Google Docs integration uses user's own Google account
|
| 248 |
+
|
| 249 |
+
### API Security
|
| 250 |
+
- Google Docs API authentication via OAuth 2.0
|
| 251 |
+
- Secure handling of API credentials
|
| 252 |
+
- Rate limiting and quota management
|
| 253 |
+
|
| 254 |
+
### File Security
|
| 255 |
+
- Generated Word documents include no executable content
|
| 256 |
+
- Sanitization of user input in document titles
|
| 257 |
+
- Secure temporary file handling during generation
|
| 258 |
+
|
| 259 |
+
## Performance Considerations
|
| 260 |
+
|
| 261 |
+
### Export Speed Optimization
|
| 262 |
+
- Lazy loading of large broadcast segments
|
| 263 |
+
- Asynchronous document generation
|
| 264 |
+
- Progress feedback for long-running exports
|
| 265 |
+
|
| 266 |
+
### Memory Management
|
| 267 |
+
- Streaming document generation for large content
|
| 268 |
+
- Cleanup of temporary files and resources
|
| 269 |
+
- Efficient handling of multilingual text encoding
|
| 270 |
+
|
| 271 |
+
### Scalability
|
| 272 |
+
- Support for exports with hundreds of broadcast segments
|
| 273 |
+
- Optimized data structures for large lecture sessions
|
| 274 |
+
- Configurable export limits to prevent system overload
|
.kiro/specs/broadcast-export/requirements.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Requirements Document
|
| 2 |
+
|
| 3 |
+
## Introduction
|
| 4 |
+
|
| 5 |
+
This feature adds export functionality to the SyncMaster Enhanced application, specifically for the broadcast section. The feature allows students to export their lecture content (original text, translations, and summaries) from a specific point in time when they press the export button, rather than exporting the entire broadcast history. This enables students to capture and save important lecture segments for later review in Word or Google Docs format.
|
| 6 |
+
|
| 7 |
+
## Requirements
|
| 8 |
+
|
| 9 |
+
### Requirement 1
|
| 10 |
+
|
| 11 |
+
**User Story:** As a student using SyncMaster during a lecture, I want to export the broadcast content from a specific moment, so that I can save important lecture segments for later review.
|
| 12 |
+
|
| 13 |
+
#### Acceptance Criteria
|
| 14 |
+
|
| 15 |
+
1. WHEN the user clicks the export button THEN the system SHALL record the current timestamp as the export starting point
|
| 16 |
+
2. WHEN exporting THEN the system SHALL include only broadcast segments that occurred after the export timestamp
|
| 17 |
+
3. WHEN exporting THEN the system SHALL include both original text and translations for each segment
|
| 18 |
+
4. IF no segments exist after the export timestamp THEN the system SHALL display a message indicating no content to export
|
| 19 |
+
|
| 20 |
+
### Requirement 2
|
| 21 |
+
|
| 22 |
+
**User Story:** As a student, I want to export my lecture content to Word format, so that I can easily review and edit the content offline.
|
| 23 |
+
|
| 24 |
+
#### Acceptance Criteria
|
| 25 |
+
|
| 26 |
+
1. WHEN the user selects Word export THEN the system SHALL generate a .docx file
|
| 27 |
+
2. WHEN generating the Word document THEN the system SHALL include a header with lecture title and export timestamp
|
| 28 |
+
3. WHEN generating the Word document THEN the system SHALL format the content with clear sections for broadcast segments and summary
|
| 29 |
+
4. WHEN the Word document is generated THEN the system SHALL provide a download link to the user
|
| 30 |
+
5. IF the Word generation fails THEN the system SHALL display an error message and suggest alternative export options
|
| 31 |
+
|
| 32 |
+
### Requirement 3
|
| 33 |
+
|
| 34 |
+
**User Story:** As a student, I want to export my lecture content to Google Docs, so that I can access and share it from anywhere.
|
| 35 |
+
|
| 36 |
+
#### Acceptance Criteria
|
| 37 |
+
|
| 38 |
+
1. WHEN the user selects Google Docs export THEN the system SHALL create a new Google Docs document
|
| 39 |
+
2. WHEN creating the Google Docs document THEN the system SHALL use the same formatting structure as Word export
|
| 40 |
+
3. WHEN the Google Docs document is created THEN the system SHALL provide a shareable link to the user
|
| 41 |
+
4. IF Google Docs integration is not available THEN the system SHALL fall back to Word export
|
| 42 |
+
5. WHEN Google Docs export fails THEN the system SHALL display an error message with troubleshooting steps
|
| 43 |
+
|
| 44 |
+
### Requirement 4
|
| 45 |
+
|
| 46 |
+
**User Story:** As a student, I want the exported content to include the Arabic summary, so that I can have a comprehensive review document.
|
| 47 |
+
|
| 48 |
+
#### Acceptance Criteria
|
| 49 |
+
|
| 50 |
+
1. WHEN exporting THEN the system SHALL include the current Arabic summary if available
|
| 51 |
+
2. WHEN no Arabic summary exists THEN the system SHALL generate a new summary based on the exported segments
|
| 52 |
+
3. WHEN generating a new summary THEN the system SHALL use the same AI translation service as the main application
|
| 53 |
+
4. IF summary generation fails THEN the system SHALL export without the summary section and notify the user
|
| 54 |
+
|
| 55 |
+
### Requirement 5
|
| 56 |
+
|
| 57 |
+
**User Story:** As a student, I want to preview the export content before downloading, so that I can verify it contains the information I need.
|
| 58 |
+
|
| 59 |
+
#### Acceptance Criteria
|
| 60 |
+
|
| 61 |
+
1. WHEN the user clicks export THEN the system SHALL display a preview modal showing the content to be exported
|
| 62 |
+
2. WHEN showing the preview THEN the system SHALL display the number of segments and estimated document length
|
| 63 |
+
3. WHEN in preview mode THEN the user SHALL be able to confirm or cancel the export
|
| 64 |
+
4. WHEN the user confirms export THEN the system SHALL proceed with the selected format
|
| 65 |
+
5. WHEN the user cancels export THEN the system SHALL close the preview without creating any files
|
| 66 |
+
|
| 67 |
+
### Requirement 6
|
| 68 |
+
|
| 69 |
+
**User Story:** As a student, I want the export feature to work in both Arabic and English interfaces, so that I can use it regardless of my language preference.
|
| 70 |
+
|
| 71 |
+
#### Acceptance Criteria
|
| 72 |
+
|
| 73 |
+
1. WHEN the interface language is Arabic THEN all export UI elements SHALL be displayed in Arabic
|
| 74 |
+
2. WHEN the interface language is English THEN all export UI elements SHALL be displayed in English
|
| 75 |
+
3. WHEN exporting THEN the document structure SHALL adapt to the interface language while preserving content languages
|
| 76 |
+
4. WHEN displaying error messages THEN they SHALL be shown in the current interface language
|
| 77 |
+
|
| 78 |
+
### Requirement 7
|
| 79 |
+
|
| 80 |
+
**User Story:** As a student, I want the export button to be easily accessible in the broadcast section, so that I can quickly export content during live lectures.
|
| 81 |
+
|
| 82 |
+
#### Acceptance Criteria
|
| 83 |
+
|
| 84 |
+
1. WHEN viewing the broadcast section THEN the export button SHALL be prominently displayed
|
| 85 |
+
2. WHEN the broadcast section is collapsed THEN the export button SHALL remain visible
|
| 86 |
+
3. WHEN clicking the export button THEN the system SHALL respond within 2 seconds
|
| 87 |
+
4. WHEN no broadcast segments exist THEN the export button SHALL be disabled with an explanatory tooltip
|
.kiro/specs/broadcast-export/tasks.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Implementation Plan
|
| 2 |
+
|
| 3 |
+
- [x] 1. Set up export infrastructure and dependencies
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
- Install required Python packages for document generation (python-docx, google-api-python-client)
|
| 7 |
+
- Update requirements.txt with new dependencies
|
| 8 |
+
- Create basic project structure for export functionality
|
| 9 |
+
- _Requirements: 1.1, 2.1, 3.1_
|
| 10 |
+
|
| 11 |
+
- [ ] 2. Implement core export engine
|
| 12 |
+
- [x] 2.1 Create BroadcastExporter class with timestamp filtering
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
- Write BroadcastExporter class in new exporter.py file
|
| 17 |
+
- Implement filter_segments_from_timestamp method to filter segments by export time
|
| 18 |
+
- Add prepare_export_content method to structure data for export
|
| 19 |
+
- Create unit tests for timestamp filtering logic
|
| 20 |
+
- _Requirements: 1.1, 1.2, 1.3_
|
| 21 |
+
|
| 22 |
+
- [ ] 2.2 Implement Word document generation functionality
|
| 23 |
+
- Add export_to_word method using python-docx library
|
| 24 |
+
- Create document template with proper Arabic/English formatting
|
| 25 |
+
- Implement structured content layout (header, segments, summary sections)
|
| 26 |
+
- Add proper RTL text support for Arabic content
|
| 27 |
+
- Write unit tests for Word document generation
|
| 28 |
+
- _Requirements: 2.1, 2.2, 2.3, 6.3_
|
| 29 |
+
|
| 30 |
+
- [ ] 2.3 Add export content preparation and formatting
|
| 31 |
+
- Implement content structuring for both original text and translations
|
| 32 |
+
- Add timestamp formatting for human-readable time ranges
|
| 33 |
+
- Create multilingual document headers and section titles
|
| 34 |
+
- Handle special characters and Unicode text properly
|
| 35 |
+
- Write tests for content preparation logic
|
| 36 |
+
- _Requirements: 1.3, 2.3, 6.1, 6.2_
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
- [ ] 3. Integrate export functionality with existing UI
|
| 40 |
+
- [ ] 3.1 Add export button to broadcast section
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
- Modify app.py to add export button in broadcast expander
|
| 44 |
+
- Implement export timestamp recording when button is clicked
|
| 45 |
+
- Add button state management (enabled/disabled based on content)
|
| 46 |
+
- Create multilingual button text and tooltips
|
| 47 |
+
- _Requirements: 7.1, 7.2, 7.4, 6.1, 6.2_
|
| 48 |
+
|
| 49 |
+
- [ ] 3.2 Create export preview modal interface
|
| 50 |
+
- Implement export preview modal using Streamlit components
|
| 51 |
+
- Add content preview showing segments count and estimated length
|
| 52 |
+
- Create format selection interface (Word/Google Docs options)
|
| 53 |
+
- Add confirm/cancel buttons with proper event handling
|
| 54 |
+
- Write UI tests for modal functionality
|
| 55 |
+
- _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5_
|
| 56 |
+
|
| 57 |
+
- [ ] 3.3 Implement export progress and feedback system
|
| 58 |
+
- Add progress indicators during export generation
|
| 59 |
+
- Create success/error message display system
|
| 60 |
+
- Implement download link generation for completed exports
|
| 61 |
+
- Add multilingual error messages and user feedback
|
| 62 |
+
- Handle export cancellation and cleanup
|
| 63 |
+
- _Requirements: 7.3, 6.4, 2.4_
|
| 64 |
+
|
| 65 |
+
- [ ] 4. Add summary generation for export content
|
| 66 |
+
- [ ] 4.1 Integrate AI summary generation for export segments
|
| 67 |
+
- Extend exporter.py with generate_export_summary method
|
| 68 |
+
- Use existing translator.py functionality for summary generation
|
| 69 |
+
- Implement summary generation based on filtered segments only
|
| 70 |
+
- Add fallback handling when summary generation fails
|
| 71 |
+
- Create tests for summary integration
|
| 72 |
+
- _Requirements: 4.1, 4.2, 4.3, 4.4_
|
| 73 |
+
|
| 74 |
+
- [ ] 4.2 Handle summary inclusion in export documents
|
| 75 |
+
- Modify Word document generation to include summary section
|
| 76 |
+
- Add conditional summary inclusion based on user preferences
|
| 77 |
+
- Implement proper formatting for Arabic summary text
|
| 78 |
+
- Handle cases where summary is unavailable or generation fails
|
| 79 |
+
- Test summary formatting in exported documents
|
| 80 |
+
- _Requirements: 4.1, 4.4, 6.3_
|
| 81 |
+
|
| 82 |
+
- [ ] 5. Implement Google Docs integration
|
| 83 |
+
- [x] 5.1 Set up Google Docs API integration
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
- Create Google Docs API service setup and authentication
|
| 88 |
+
- Implement OAuth 2.0 flow for user authorization
|
| 89 |
+
- Add Google API credentials management
|
| 90 |
+
- Create basic Google Docs document creation functionality
|
| 91 |
+
- Write integration tests for Google API connectivity
|
| 92 |
+
- _Requirements: 3.1, 3.2_
|
| 93 |
+
|
| 94 |
+
- [ ] 5.2 Implement Google Docs export functionality
|
| 95 |
+
- Add export_to_google_docs method to BroadcastExporter
|
| 96 |
+
- Implement document formatting using Google Docs API
|
| 97 |
+
- Add proper RTL text support for Arabic content in Google Docs
|
| 98 |
+
- Create shareable link generation for exported documents
|
| 99 |
+
- Handle Google Docs API errors and rate limiting
|
| 100 |
+
- _Requirements: 3.1, 3.2, 3.3, 6.3_
|
| 101 |
+
|
| 102 |
+
- [ ] 5.3 Add Google Docs fallback and error handling
|
| 103 |
+
- Implement automatic fallback to Word export when Google Docs fails
|
| 104 |
+
- Add user-friendly error messages for Google Docs issues
|
| 105 |
+
- Create retry mechanisms for transient API failures
|
| 106 |
+
- Handle authentication errors and re-authorization flow
|
| 107 |
+
- Test fallback scenarios and error recovery
|
| 108 |
+
- _Requirements: 3.4, 3.5_
|
| 109 |
+
|
| 110 |
+
- [ ] 6. Implement comprehensive error handling
|
| 111 |
+
- [ ] 6.1 Add export validation and error prevention
|
| 112 |
+
- Implement pre-export validation (timestamp, content availability)
|
| 113 |
+
- Add user input sanitization for document titles and content
|
| 114 |
+
- Create validation for export configuration parameters
|
| 115 |
+
- Handle edge cases (empty segments, invalid timestamps)
|
| 116 |
+
- Write comprehensive validation tests
|
| 117 |
+
- _Requirements: 1.4, 7.4_
|
| 118 |
+
|
| 119 |
+
- [ ] 6.2 Create robust error recovery system
|
| 120 |
+
- Implement try-catch blocks for all export operations
|
| 121 |
+
- Add logging for debugging export failures
|
| 122 |
+
- Create user-friendly error messages in multiple languages
|
| 123 |
+
- Implement cleanup procedures for failed exports
|
| 124 |
+
- Add error reporting and diagnostics functionality
|
| 125 |
+
- _Requirements: 2.5, 3.5, 6.4_
|
| 126 |
+
|
| 127 |
+
- [ ] 7. Add multilingual support and localization
|
| 128 |
+
- [ ] 7.1 Implement Arabic interface support for export features
|
| 129 |
+
- Add Arabic translations for all export UI elements
|
| 130 |
+
- Create RTL-compatible export modal layout
|
| 131 |
+
- Implement Arabic document templates and formatting
|
| 132 |
+
- Add Arabic error messages and user feedback
|
| 133 |
+
- Test Arabic interface functionality thoroughly
|
| 134 |
+
- _Requirements: 6.1, 6.4_
|
| 135 |
+
|
| 136 |
+
- [ ] 7.2 Ensure consistent multilingual document generation
|
| 137 |
+
- Implement language-aware document formatting
|
| 138 |
+
- Add proper font selection for Arabic and English text
|
| 139 |
+
- Create consistent styling across different languages
|
| 140 |
+
- Handle mixed-language content in exports
|
| 141 |
+
- Test document generation with various language combinations
|
| 142 |
+
- _Requirements: 6.3_
|
| 143 |
+
|
| 144 |
+
Auto-process snapshots (keeps recording)
|
| 145 |
+
- [ ] 8. Create comprehensive testing suite
|
| 146 |
+
- [ ] 8.1 Write unit tests for export functionality
|
| 147 |
+
- Create tests for BroadcastExporter class methods
|
| 148 |
+
- Add tests for timestamp filtering and content preparation
|
| 149 |
+
- Write tests for Word document generation
|
| 150 |
+
- Create tests for error handling and edge cases
|
| 151 |
+
- Implement test data fixtures for various scenarios
|
| 152 |
+
- _Requirements: All requirements validation_
|
| 153 |
+
|
| 154 |
+
- [ ] 8.2 Implement integration tests for complete export flow
|
| 155 |
+
- Create end-to-end tests from button click to document download
|
| 156 |
+
- Add tests for multilingual export scenarios
|
| 157 |
+
- Write tests for Google Docs integration (with mocking)
|
| 158 |
+
- Create performance tests for large broadcast segments
|
| 159 |
+
- Implement user workflow simulation tests
|
| 160 |
+
- _Requirements: All requirements validation_
|
| 161 |
+
|
| 162 |
+
- [ ] 9. Optimize performance and user experience
|
| 163 |
+
- [ ] 9.1 Implement export performance optimizations
|
| 164 |
+
- Add asynchronous processing for large exports
|
| 165 |
+
- Implement memory-efficient document generation
|
| 166 |
+
- Create progress tracking for long-running exports
|
| 167 |
+
- Add export size limits and warnings
|
| 168 |
+
- Optimize data structures for large segment collections
|
| 169 |
+
- _Requirements: 7.3_
|
| 170 |
+
|
| 171 |
+
- [ ] 9.2 Enhance user experience and accessibility
|
| 172 |
+
- Add keyboard navigation support for export interface
|
| 173 |
+
- Implement screen reader compatibility
|
| 174 |
+
- Create helpful tooltips and user guidance
|
| 175 |
+
- Add export history and recent exports tracking
|
| 176 |
+
- Implement user preferences for export settings
|
| 177 |
+
- _Requirements: 5.1, 7.1, 7.2_
|
| 178 |
+
|
| 179 |
+
- [ ] 10. Final integration and documentation
|
| 180 |
+
- [ ] 10.1 Complete integration with main application
|
| 181 |
+
- Ensure seamless integration with existing broadcast functionality
|
| 182 |
+
- Test compatibility with all existing features
|
| 183 |
+
- Verify proper session state management
|
| 184 |
+
- Add configuration options for export features
|
| 185 |
+
- Create deployment-ready code with proper error handling
|
| 186 |
+
- _Requirements: All requirements_
|
| 187 |
+
|
| 188 |
+
- [ ] 10.2 Create user documentation and help content
|
| 189 |
+
- Write user guide for export functionality
|
| 190 |
+
- Create troubleshooting documentation
|
| 191 |
+
- Add inline help text and tooltips
|
| 192 |
+
- Create video tutorials or screenshots for complex workflows
|
| 193 |
+
- Document API integration requirements for Google Docs
|
| 194 |
+
- _Requirements: 3.5, 6.4_
|
.kiro/specs/model-management/design.md
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Model Management System Design
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
The Model Management System provides a centralized, robust approach to managing AI model configurations across multiple providers (Gemini, OpenRouter, Groq). It includes automatic validation, fallback mechanisms, health monitoring, and user-friendly status reporting.
|
| 6 |
+
|
| 7 |
+
## Architecture
|
| 8 |
+
|
| 9 |
+
### Core Components
|
| 10 |
+
|
| 11 |
+
1. **ModelManager**: Central coordinator for all model operations
|
| 12 |
+
2. **ModelValidator**: Validates model configurations against provider APIs
|
| 13 |
+
3. **ModelHealthMonitor**: Monitors model availability and performance
|
| 14 |
+
4. **FallbackChain**: Manages automatic fallback to alternative models
|
| 15 |
+
5. **ModelConfigStore**: Centralized configuration management
|
| 16 |
+
6. **StatusReporter**: Provides real-time status information to users
|
| 17 |
+
|
| 18 |
+
### Component Interactions
|
| 19 |
+
|
| 20 |
+
```
|
| 21 |
+
User Request → ModelManager → ModelValidator → Provider API
|
| 22 |
+
↓
|
| 23 |
+
FallbackChain → Alternative Models
|
| 24 |
+
↓
|
| 25 |
+
StatusReporter → User Feedback
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
## Components and Interfaces
|
| 29 |
+
|
| 30 |
+
### ModelManager
|
| 31 |
+
|
| 32 |
+
**Purpose**: Central coordinator for all AI model operations
|
| 33 |
+
|
| 34 |
+
**Key Methods**:
|
| 35 |
+
- `get_available_model(provider, task_type)`: Returns best available model for a task
|
| 36 |
+
- `execute_with_fallback(prompt, preferences)`: Executes request with automatic fallback
|
| 37 |
+
- `get_system_status()`: Returns current status of all models
|
| 38 |
+
- `refresh_configurations()`: Reloads and validates all configurations
|
| 39 |
+
|
| 40 |
+
**Interfaces**:
|
| 41 |
+
- Input: User requests, configuration updates
|
| 42 |
+
- Output: Model responses, status information, error messages
|
| 43 |
+
|
| 44 |
+
### ModelValidator
|
| 45 |
+
|
| 46 |
+
**Purpose**: Validates model configurations against provider APIs
|
| 47 |
+
|
| 48 |
+
**Key Methods**:
|
| 49 |
+
- `validate_model(provider, model_id)`: Tests if a model is valid and accessible
|
| 50 |
+
- `validate_all_models()`: Validates all configured models
|
| 51 |
+
- `suggest_alternatives(invalid_model)`: Suggests working alternatives for invalid models
|
| 52 |
+
- `update_model_status(model, status)`: Updates model availability status
|
| 53 |
+
|
| 54 |
+
**Validation Process**:
|
| 55 |
+
1. Send minimal test request to provider API
|
| 56 |
+
2. Check response for success/error patterns
|
| 57 |
+
3. Update model status based on results
|
| 58 |
+
4. Log validation results with timestamps
|
| 59 |
+
|
| 60 |
+
### ModelHealthMonitor
|
| 61 |
+
|
| 62 |
+
**Purpose**: Continuously monitors model health and availability
|
| 63 |
+
|
| 64 |
+
**Key Methods**:
|
| 65 |
+
- `start_monitoring()`: Begins periodic health checks
|
| 66 |
+
- `check_model_health(model)`: Performs health check on specific model
|
| 67 |
+
- `handle_model_failure(model, error)`: Responds to model failures
|
| 68 |
+
- `get_health_report()`: Returns comprehensive health status
|
| 69 |
+
|
| 70 |
+
**Monitoring Strategy**:
|
| 71 |
+
- Periodic health checks every 5 minutes
|
| 72 |
+
- Immediate checks after failures
|
| 73 |
+
- Exponential backoff for failed models
|
| 74 |
+
- Automatic recovery detection
|
| 75 |
+
|
| 76 |
+
### FallbackChain
|
| 77 |
+
|
| 78 |
+
**Purpose**: Manages automatic fallback to alternative models
|
| 79 |
+
|
| 80 |
+
**Fallback Priority**:
|
| 81 |
+
1. **Primary Models**: User-configured preferred models
|
| 82 |
+
2. **Secondary Models**: Validated working alternatives
|
| 83 |
+
3. **Emergency Models**: Always-available simple response system
|
| 84 |
+
|
| 85 |
+
**Fallback Logic**:
|
| 86 |
+
```
|
| 87 |
+
Primary Model → Secondary Models → Emergency Response
|
| 88 |
+
↓ ↓ ↓
|
| 89 |
+
Full AI Reduced AI Rule-based
|
| 90 |
+
Response Response Response
|
| 91 |
+
```
|
| 92 |
+
|
| 93 |
+
### ModelConfigStore
|
| 94 |
+
|
| 95 |
+
**Purpose**: Centralized configuration management
|
| 96 |
+
|
| 97 |
+
**Configuration Structure**:
|
| 98 |
+
```json
|
| 99 |
+
{
|
| 100 |
+
"providers": {
|
| 101 |
+
"openrouter": {
|
| 102 |
+
"api_key": "...",
|
| 103 |
+
"models": {
|
| 104 |
+
"primary": "meta-llama/llama-3.2-3b-instruct:free",
|
| 105 |
+
"fallbacks": [
|
| 106 |
+
"meta-llama/llama-3.1-8b-instruct:free",
|
| 107 |
+
"google/gemma-2-9b-it:free"
|
| 108 |
+
]
|
| 109 |
+
}
|
| 110 |
+
},
|
| 111 |
+
"groq": {
|
| 112 |
+
"api_key": "...",
|
| 113 |
+
"models": {
|
| 114 |
+
"primary": "llama-3.3-70b-versatile",
|
| 115 |
+
"fallbacks": ["mixtral-8x7b-32768"]
|
| 116 |
+
}
|
| 117 |
+
}
|
| 118 |
+
}
|
| 119 |
+
}
|
| 120 |
+
```
|
| 121 |
+
|
| 122 |
+
## Data Models
|
| 123 |
+
|
| 124 |
+
### ModelStatus
|
| 125 |
+
```python
|
| 126 |
+
@dataclass
|
| 127 |
+
class ModelStatus:
|
| 128 |
+
provider: str
|
| 129 |
+
model_id: str
|
| 130 |
+
status: str # 'available', 'unavailable', 'quota_exceeded', 'error'
|
| 131 |
+
last_checked: datetime
|
| 132 |
+
error_message: Optional[str]
|
| 133 |
+
response_time_ms: Optional[int]
|
| 134 |
+
success_rate: float
|
| 135 |
+
```
|
| 136 |
+
|
| 137 |
+
### ProviderConfig
|
| 138 |
+
```python
|
| 139 |
+
@dataclass
|
| 140 |
+
class ProviderConfig:
|
| 141 |
+
name: str
|
| 142 |
+
api_key: str
|
| 143 |
+
base_url: str
|
| 144 |
+
primary_models: List[str]
|
| 145 |
+
fallback_models: List[str]
|
| 146 |
+
timeout_seconds: int = 30
|
| 147 |
+
retry_attempts: int = 3
|
| 148 |
+
```
|
| 149 |
+
|
| 150 |
+
### ValidationResult
|
| 151 |
+
```python
|
| 152 |
+
@dataclass
|
| 153 |
+
class ValidationResult:
|
| 154 |
+
model_id: str
|
| 155 |
+
is_valid: bool
|
| 156 |
+
error_message: Optional[str]
|
| 157 |
+
suggested_alternatives: List[str]
|
| 158 |
+
validation_timestamp: datetime
|
| 159 |
+
```
|
| 160 |
+
|
| 161 |
+
## Error Handling
|
| 162 |
+
|
| 163 |
+
### Error Categories
|
| 164 |
+
|
| 165 |
+
1. **Configuration Errors**: Invalid API keys, malformed model IDs
|
| 166 |
+
2. **Network Errors**: Timeout, connection failures
|
| 167 |
+
3. **Provider Errors**: Rate limits, quota exceeded, model unavailable
|
| 168 |
+
4. **Validation Errors**: Model not found, unsupported parameters
|
| 169 |
+
|
| 170 |
+
### Error Response Strategy
|
| 171 |
+
|
| 172 |
+
1. **Immediate Fallback**: Switch to next available model
|
| 173 |
+
2. **User Notification**: Inform user of fallback with clear messaging
|
| 174 |
+
3. **Automatic Recovery**: Retry failed models after cooldown period
|
| 175 |
+
4. **Graceful Degradation**: Provide simple responses when all AI fails
|
| 176 |
+
|
| 177 |
+
### Error Logging
|
| 178 |
+
|
| 179 |
+
- Structured logging with error categories
|
| 180 |
+
- Performance metrics tracking
|
| 181 |
+
- User-friendly error messages
|
| 182 |
+
- Detailed technical logs for debugging
|
| 183 |
+
|
| 184 |
+
## Testing Strategy
|
| 185 |
+
|
| 186 |
+
### Unit Tests
|
| 187 |
+
- Model validation logic
|
| 188 |
+
- Fallback chain behavior
|
| 189 |
+
- Configuration parsing
|
| 190 |
+
- Error handling scenarios
|
| 191 |
+
|
| 192 |
+
### Integration Tests
|
| 193 |
+
- End-to-end model requests
|
| 194 |
+
- Provider API interactions
|
| 195 |
+
- Fallback mechanisms
|
| 196 |
+
- Configuration updates
|
| 197 |
+
|
| 198 |
+
### Performance Tests
|
| 199 |
+
- Response time monitoring
|
| 200 |
+
- Concurrent request handling
|
| 201 |
+
- Memory usage optimization
|
| 202 |
+
- Fallback performance impact
|
| 203 |
+
|
| 204 |
+
### User Acceptance Tests
|
| 205 |
+
- Model status visibility
|
| 206 |
+
- Error message clarity
|
| 207 |
+
- Fallback transparency
|
| 208 |
+
- Configuration management UI
|
| 209 |
+
|
| 210 |
+
## Implementation Phases
|
| 211 |
+
|
| 212 |
+
### Phase 1: Core Infrastructure
|
| 213 |
+
- ModelManager implementation
|
| 214 |
+
- Basic validation system
|
| 215 |
+
- Simple fallback mechanism
|
| 216 |
+
|
| 217 |
+
### Phase 2: Advanced Features
|
| 218 |
+
- Health monitoring
|
| 219 |
+
- Comprehensive error handling
|
| 220 |
+
- Performance optimization
|
| 221 |
+
|
| 222 |
+
### Phase 3: User Experience
|
| 223 |
+
- Status dashboard
|
| 224 |
+
- Configuration UI
|
| 225 |
+
- Advanced monitoring features
|
.kiro/specs/model-management/requirements.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Requirements Document
|
| 2 |
+
|
| 3 |
+
## Introduction
|
| 4 |
+
|
| 5 |
+
This feature aims to create a robust model management system that prevents invalid model configurations, provides automatic fallback mechanisms, and ensures reliable AI service availability. The system should handle model validation, automatic updates, and graceful error handling to prevent service disruptions.
|
| 6 |
+
|
| 7 |
+
## Requirements
|
| 8 |
+
|
| 9 |
+
### Requirement 1
|
| 10 |
+
|
| 11 |
+
**User Story:** As a system administrator, I want automatic model validation so that invalid model configurations are detected and corrected before they cause service failures.
|
| 12 |
+
|
| 13 |
+
#### Acceptance Criteria
|
| 14 |
+
|
| 15 |
+
1. WHEN the system starts THEN it SHALL validate all configured AI models against their respective service APIs
|
| 16 |
+
2. WHEN an invalid model is detected THEN the system SHALL automatically replace it with a known working fallback model
|
| 17 |
+
3. WHEN model validation fails THEN the system SHALL log the error and continue with fallback models
|
| 18 |
+
4. WHEN a model becomes unavailable THEN the system SHALL automatically switch to the next available model in the priority list
|
| 19 |
+
|
| 20 |
+
### Requirement 2
|
| 21 |
+
|
| 22 |
+
**User Story:** As a developer, I want centralized model configuration management so that all AI services use consistent and validated model settings.
|
| 23 |
+
|
| 24 |
+
#### Acceptance Criteria
|
| 25 |
+
|
| 26 |
+
1. WHEN the application initializes THEN it SHALL load model configurations from a centralized configuration system
|
| 27 |
+
2. WHEN model configurations are updated THEN all AI services SHALL automatically use the new configurations
|
| 28 |
+
3. WHEN a service requests a model THEN the system SHALL provide the most appropriate available model based on priority and availability
|
| 29 |
+
4. WHEN model configurations are invalid THEN the system SHALL provide clear error messages and suggested fixes
|
| 30 |
+
|
| 31 |
+
### Requirement 3
|
| 32 |
+
|
| 33 |
+
**User Story:** As an end user, I want transparent model status information so that I understand which AI services are available and their current status.
|
| 34 |
+
|
| 35 |
+
#### Acceptance Criteria
|
| 36 |
+
|
| 37 |
+
1. WHEN I access the application THEN I SHALL see the current status of all AI models (available, unavailable, quota exceeded, etc.)
|
| 38 |
+
2. WHEN a model fails THEN I SHALL receive a clear notification about the fallback being used
|
| 39 |
+
3. WHEN model status changes THEN the UI SHALL update to reflect the current availability
|
| 40 |
+
4. WHEN I encounter an AI error THEN I SHALL receive helpful information about alternative options
|
| 41 |
+
|
| 42 |
+
### Requirement 4
|
| 43 |
+
|
| 44 |
+
**User Story:** As a system operator, I want automatic model health monitoring so that model issues are detected and resolved proactively.
|
| 45 |
+
|
| 46 |
+
#### Acceptance Criteria
|
| 47 |
+
|
| 48 |
+
1. WHEN the system is running THEN it SHALL periodically check the health of all configured AI models
|
| 49 |
+
2. WHEN a model health check fails THEN the system SHALL attempt to use alternative models
|
| 50 |
+
3. WHEN all models for a service fail THEN the system SHALL provide graceful degradation with simple responses
|
| 51 |
+
4. WHEN model health is restored THEN the system SHALL automatically resume using the preferred models
|
| 52 |
+
|
| 53 |
+
### Requirement 5
|
| 54 |
+
|
| 55 |
+
**User Story:** As a developer, I want comprehensive error handling and logging so that model-related issues can be quickly diagnosed and resolved.
|
| 56 |
+
|
| 57 |
+
#### Acceptance Criteria
|
| 58 |
+
|
| 59 |
+
1. WHEN a model error occurs THEN the system SHALL log detailed error information including model name, error type, and suggested resolution
|
| 60 |
+
2. WHEN fallback models are used THEN the system SHALL log the fallback chain and reasons for each fallback
|
| 61 |
+
3. WHEN model configurations are automatically corrected THEN the system SHALL log the changes made
|
| 62 |
+
4. WHEN users encounter model errors THEN they SHALL receive user-friendly error messages with actionable guidance
|
.kiro/specs/model-management/tasks.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Implementation Plan
|
| 2 |
+
|
| 3 |
+
- [ ] 1. Create core model management infrastructure
|
| 4 |
+
- Implement ModelStatus and ProviderConfig data classes
|
| 5 |
+
- Create ModelConfigStore for centralized configuration management
|
| 6 |
+
- Set up basic logging and error handling framework
|
| 7 |
+
- _Requirements: 2.1, 2.2, 5.1_
|
| 8 |
+
|
| 9 |
+
- [ ] 2. Implement ModelValidator component
|
| 10 |
+
- Create ModelValidator class with validation methods
|
| 11 |
+
- Implement validate_model() method for individual model testing
|
| 12 |
+
- Add validate_all_models() method for batch validation
|
| 13 |
+
- Create suggest_alternatives() method for fallback recommendations
|
| 14 |
+
- Write unit tests for validation logic
|
| 15 |
+
- _Requirements: 1.1, 1.2, 1.3_
|
| 16 |
+
|
| 17 |
+
- [ ] 3. Build FallbackChain system
|
| 18 |
+
- Implement FallbackChain class with priority-based model selection
|
| 19 |
+
- Create fallback logic that tries primary, secondary, and emergency models
|
| 20 |
+
- Add automatic model switching when failures occur
|
| 21 |
+
- Implement graceful degradation to simple responses
|
| 22 |
+
- Write tests for fallback scenarios
|
| 23 |
+
- _Requirements: 1.4, 4.2, 4.3_
|
| 24 |
+
|
| 25 |
+
- [ ] 4. Create ModelManager central coordinator
|
| 26 |
+
- Implement ModelManager class as main interface
|
| 27 |
+
- Add get_available_model() method for model selection
|
| 28 |
+
- Create execute_with_fallback() method for request handling
|
| 29 |
+
- Implement get_system_status() for status reporting
|
| 30 |
+
- Add refresh_configurations() for dynamic config updates
|
| 31 |
+
- Write integration tests for ModelManager
|
| 32 |
+
- _Requirements: 2.3, 3.2, 4.1_
|
| 33 |
+
|
| 34 |
+
- [ ] 5. Implement ModelHealthMonitor
|
| 35 |
+
- Create ModelHealthMonitor class for continuous monitoring
|
| 36 |
+
- Add periodic health check functionality (5-minute intervals)
|
| 37 |
+
- Implement check_model_health() for individual model testing
|
| 38 |
+
- Create handle_model_failure() for failure response
|
| 39 |
+
- Add exponential backoff for failed models
|
| 40 |
+
- Write tests for health monitoring scenarios
|
| 41 |
+
- _Requirements: 4.1, 4.2, 4.4_
|
| 42 |
+
|
| 43 |
+
- [ ] 6. Build StatusReporter for user feedback
|
| 44 |
+
- Create StatusReporter class for user-facing status information
|
| 45 |
+
- Implement real-time status updates for UI
|
| 46 |
+
- Add user-friendly error message generation
|
| 47 |
+
- Create status dashboard data formatting
|
| 48 |
+
- Implement notification system for model changes
|
| 49 |
+
- Write tests for status reporting functionality
|
| 50 |
+
- _Requirements: 3.1, 3.2, 3.3, 3.4_
|
| 51 |
+
|
| 52 |
+
- [ ] 7. Integrate with existing AI services
|
| 53 |
+
- Update AITranslator to use ModelManager
|
| 54 |
+
- Modify AIQuestionEngine to use new model management
|
| 55 |
+
- Update all AI service calls to use execute_with_fallback()
|
| 56 |
+
- Replace hardcoded model configurations with centralized config
|
| 57 |
+
- Add model status display to existing UI components
|
| 58 |
+
- _Requirements: 2.2, 2.3, 3.1_
|
| 59 |
+
|
| 60 |
+
- [ ] 8. Implement comprehensive error handling
|
| 61 |
+
- Add structured error logging with categories
|
| 62 |
+
- Create user-friendly error messages for each error type
|
| 63 |
+
- Implement automatic error recovery mechanisms
|
| 64 |
+
- Add performance metrics tracking
|
| 65 |
+
- Create error reporting dashboard
|
| 66 |
+
- Write tests for all error scenarios
|
| 67 |
+
- _Requirements: 5.1, 5.2, 5.3, 5.4_
|
| 68 |
+
|
| 69 |
+
- [ ] 9. Add configuration management features
|
| 70 |
+
- Create configuration validation on startup
|
| 71 |
+
- Implement automatic model configuration updates
|
| 72 |
+
- Add configuration backup and restore functionality
|
| 73 |
+
- Create configuration migration tools for updates
|
| 74 |
+
- Add configuration validation UI
|
| 75 |
+
- Write tests for configuration management
|
| 76 |
+
- _Requirements: 2.1, 2.4, 1.2_
|
| 77 |
+
|
| 78 |
+
- [ ] 10. Create monitoring and analytics
|
| 79 |
+
- Implement performance metrics collection
|
| 80 |
+
- Add model usage statistics tracking
|
| 81 |
+
- Create health monitoring dashboard
|
| 82 |
+
- Add alerting for critical model failures
|
| 83 |
+
- Implement trend analysis for model performance
|
| 84 |
+
- Write tests for monitoring functionality
|
| 85 |
+
- _Requirements: 4.1, 4.4, 5.1_
|
| 86 |
+
|
| 87 |
+
- [ ] 11. Build user interface components
|
| 88 |
+
- Create model status display widget
|
| 89 |
+
- Add model selection interface for users
|
| 90 |
+
- Implement error notification system
|
| 91 |
+
- Create configuration management UI
|
| 92 |
+
- Add health monitoring dashboard
|
| 93 |
+
- Write UI tests for all components
|
| 94 |
+
- _Requirements: 3.1, 3.2, 3.3, 3.4_
|
| 95 |
+
|
| 96 |
+
- [ ] 12. Implement final integration and testing
|
| 97 |
+
- Integrate all components into main application
|
| 98 |
+
- Run comprehensive end-to-end tests
|
| 99 |
+
- Perform load testing with multiple concurrent requests
|
| 100 |
+
- Test all fallback scenarios under various failure conditions
|
| 101 |
+
- Validate user experience with real-world usage patterns
|
| 102 |
+
- Create deployment and maintenance documentation
|
| 103 |
+
- _Requirements: All requirements validation_
|
Dockerfile
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#
|
| 2 |
+
# -- Dockerfile for Streamlit app --
|
| 3 |
+
#
|
| 4 |
+
|
| 5 |
+
# Base image
|
| 6 |
+
FROM python:3.9-slim
|
| 7 |
+
|
| 8 |
+
# Set working directory
|
| 9 |
+
WORKDIR /app
|
| 10 |
+
|
| 11 |
+
# Install system dependencies (including ffmpeg)
|
| 12 |
+
RUN apt-get update && apt-get install -y \
|
| 13 |
+
build-essential \
|
| 14 |
+
ffmpeg \
|
| 15 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 16 |
+
|
| 17 |
+
# Copy requirements file
|
| 18 |
+
COPY requirements.txt ./requirements.txt
|
| 19 |
+
|
| 20 |
+
# Install Python dependencies
|
| 21 |
+
RUN pip install --no-cache-dir --upgrade pip
|
| 22 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 23 |
+
|
| 24 |
+
# Copy the entire app
|
| 25 |
+
COPY . .
|
| 26 |
+
|
| 27 |
+
# Create .streamlit directory and set permissions
|
| 28 |
+
RUN mkdir -p /app/.streamlit && \
|
| 29 |
+
chmod -R 755 /app/.streamlit
|
| 30 |
+
|
| 31 |
+
# Set environment variable for Streamlit config
|
| 32 |
+
ENV STREAMLIT_CONFIG_DIR=/app/.streamlit
|
| 33 |
+
|
| 34 |
+
# Expose the port that Streamlit runs on
|
| 35 |
+
EXPOSE 8501
|
| 36 |
+
|
| 37 |
+
# Add a health check
|
| 38 |
+
HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
|
| 39 |
+
|
| 40 |
+
# Command to run the app
|
| 41 |
+
ENTRYPOINT ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]
|
FIX_GOOGLE_ACCESS.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🔧 حل مشكلة Google Access
|
| 2 |
+
|
| 3 |
+
## 🎯 المشكلة:
|
| 4 |
+
```
|
| 5 |
+
Error 403: access_denied
|
| 6 |
+
SyncMaster Export has not completed the Google verification process
|
| 7 |
+
```
|
| 8 |
+
|
| 9 |
+
## ✅ الحل السريع:
|
| 10 |
+
|
| 11 |
+
### 1️⃣ إضافة نفسك كـ Test User:
|
| 12 |
+
1. اذهب إلى [Google Cloud Console](https://console.cloud.google.com/)
|
| 13 |
+
2. اختر مشروع `syncmaster-export`
|
| 14 |
+
3. اذهب إلى: **APIs & Services** → **OAuth consent screen**
|
| 15 |
+
4. اضغط على **"ADD USERS"** في قسم "Test users"
|
| 16 |
+
5. أضف إيميل Google الخاص بك
|
| 17 |
+
6. اضغط **"SAVE"**
|
| 18 |
+
|
| 19 |
+
### 2️⃣ أو: نشر التطبيق للعامة (أسرع):
|
| 20 |
+
1. في نفس صفحة **OAuth consent screen**
|
| 21 |
+
2. اضغط **"PUBLISH APP"**
|
| 22 |
+
3. اضغط **"CONFIRM"**
|
| 23 |
+
4. الآن يمكن لأي شخص استخدام التطبيق
|
| 24 |
+
|
| 25 |
+
## 🔧 حل مشكلة المنفذ:
|
| 26 |
+
|
| 27 |
+
المشكلة أن Streamlit يستخدم المنفذ 8501، لذلك سنغير المنفذ للمصادقة.
|
GOOGLE_SETUP.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# إعداد Google Docs للتصدير المباشر
|
| 2 |
+
|
| 3 |
+
## الخطوات المطلوبة:
|
| 4 |
+
|
| 5 |
+
### 1. إنشاء مشروع Google Cloud
|
| 6 |
+
1. اذهب إلى [Google Cloud Console](https://console.cloud.google.com/)
|
| 7 |
+
2. أنشئ مشروع جديد أو اختر مشروع موجود
|
| 8 |
+
3. اكتب اسم المشروع (مثل: "SyncMaster Export")
|
| 9 |
+
|
| 10 |
+
### 2. تفعيل Google Docs API
|
| 11 |
+
1. في القائمة الجانبية، اذهب إلى "APIs & Services" > "Library"
|
| 12 |
+
2. ابحث عن "Google Docs API"
|
| 13 |
+
3. اضغط على "Enable"
|
| 14 |
+
|
| 15 |
+
### 3. إنشاء بيانات الاعتماد
|
| 16 |
+
1. اذهب إلى "APIs & Services" > "Credentials"
|
| 17 |
+
2. اضغط على "Create Credentials" > "OAuth 2.0 Client ID"
|
| 18 |
+
3. إذا لم تكن قد أعددت شاشة الموافقة، ستحتاج لإعدادها:
|
| 19 |
+
- اختر "External" للمستخدمين العاديين
|
| 20 |
+
- املأ المعلومات المطلوبة (اسم التطبيق، إيميل الدعم)
|
| 21 |
+
- أضف نطاقات Google Docs
|
| 22 |
+
4. اختر "Desktop application" كنوع التطبيق
|
| 23 |
+
5. اكتب اسم العميل (مثل: "SyncMaster Desktop")
|
| 24 |
+
6. اضغط "Create"
|
| 25 |
+
|
| 26 |
+
### 4. تحميل ملف بيانات الاعتماد
|
| 27 |
+
1. بعد إنشاء بيانات الاعتماد، اضغط على أيقونة التحميل
|
| 28 |
+
2. احفظ الملف باسم `credentials.json`
|
| 29 |
+
3. ضع الملف في نفس مجلد التطبيق
|
| 30 |
+
|
| 31 |
+
### 5. تشغيل التطبيق
|
| 32 |
+
1. شغل التطبيق: `streamlit run app.py`
|
| 33 |
+
2. اضغط على زر "📤 تصدير إلى Google Docs"
|
| 34 |
+
3. ستفتح نافذة متصفح للمصادقة مع Google
|
| 35 |
+
4. سجل دخول بحساب Google الخاص بك
|
| 36 |
+
5. امنح الصلاحيات المطلوبة
|
| 37 |
+
6. ارجع للتطبيق وستجد رابط المستند الجديد
|
| 38 |
+
|
| 39 |
+
## ملاحظات مهمة:
|
| 40 |
+
- يتم حفظ بيانات المصادقة في ملف `token.json` لاستخدامها لاحقاً
|
| 41 |
+
- لا تشارك ملفات `credentials.json` أو `token.json` مع أحد
|
| 42 |
+
- يمكنك إلغاء الصلاحيات من إعدادات حساب Google في أي وقت
|
| 43 |
+
|
| 44 |
+
## استكشاف الأخطاء:
|
| 45 |
+
- إذا ظهر خطأ "credentials.json not found"، تأكد من وضع الملف في المجلد الصحيح
|
| 46 |
+
- إذا فشلت المصادقة، احذف ملف `token.json` وحاول مرة أخرى
|
| 47 |
+
- تأكد من تفعيل Google Docs API في مشروعك
|
GOOGLE_SETUP_EASY.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🚀 إعداد Google Docs - الطريقة السهلة
|
| 2 |
+
|
| 3 |
+
## 📝 الإجابات على أسئلتك:
|
| 4 |
+
|
| 5 |
+
### ❓ ماذا أدخل في هذه الحقول؟
|
| 6 |
+
```
|
| 7 |
+
Authorised JavaScript origins: اتركه فارغ (لا تدخل شيء)
|
| 8 |
+
Authorised redirect URIs: اتركه فارغ (لا تدخل شيء)
|
| 9 |
+
```
|
| 10 |
+
**السبب**: نحن ننشئ تطبيق Desktop وليس Web، لذلك لا نحتاج هذه الحقول.
|
| 11 |
+
|
| 12 |
+
### ❓ لم أستطع الوصول إلى صفحة Scopes؟
|
| 13 |
+
**الحل**: لا تقلق! يمكنك تخطي إضافة الـ Scopes يدوياً. التطبيق سيطلبها تلقائياً.
|
| 14 |
+
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
## 🎯 الطريقة المبسطة (5 دقائق فقط):
|
| 18 |
+
|
| 19 |
+
### 1️⃣ اذهب إلى Google Cloud Console
|
| 20 |
+
🔗 **الرابط**: https://console.cloud.google.com/
|
| 21 |
+
|
| 22 |
+
### 2️⃣ أنشئ مشروع جديد
|
| 23 |
+
- اضغط "Select a project" → "NEW PROJECT"
|
| 24 |
+
- اسم المشروع: `SyncMaster`
|
| 25 |
+
- اضغط "CREATE"
|
| 26 |
+
|
| 27 |
+
### 3️⃣ فعّل Google Docs API
|
| 28 |
+
- من القائمة الجانبية: "APIs & Services" → "Library"
|
| 29 |
+
- ابحث عن: `Google Docs API`
|
| 30 |
+
- اضغط على النتيجة الأولى → "ENABLE"
|
| 31 |
+
|
| 32 |
+
### 4️⃣ إعداد OAuth Consent Screen (مبسط)
|
| 33 |
+
- اذهب إلى: "APIs & Services" → "OAuth consent screen"
|
| 34 |
+
- اختر "External" → "CREATE"
|
| 35 |
+
- املأ فقط:
|
| 36 |
+
- **App name**: `SyncMaster`
|
| 37 |
+
- **User support email**: إيميلك
|
| 38 |
+
- **Developer contact information**: إيميلك
|
| 39 |
+
- اضغط "SAVE AND CONTINUE" في جميع الصفحات (لا تغير شيء آخر)
|
| 40 |
+
|
| 41 |
+
### 5️⃣ إنشاء Client ID
|
| 42 |
+
- اذهب إلى: "APIs & Services" → "Credentials"
|
| 43 |
+
- اضغط "+ CREATE CREDENTIALS" → "OAuth 2.0 Client ID"
|
| 44 |
+
- اختر "Desktop application"
|
| 45 |
+
- الاسم: `SyncMaster Desktop`
|
| 46 |
+
- **اترك جميع الحقول الأخرى فارغة**
|
| 47 |
+
- اضغط "CREATE"
|
| 48 |
+
|
| 49 |
+
### 6️⃣ تحميل الملف
|
| 50 |
+
- ستظهر نافذة منبثقة
|
| 51 |
+
- اضغط "DOWNLOAD JSON"
|
| 52 |
+
- احفظ الملف باسم `credentials.json`
|
| 53 |
+
- ضعه في مجلد التطبيق (نفس مكان app.py)
|
| 54 |
+
|
| 55 |
+
### 7️⃣ اختبار
|
| 56 |
+
```bash
|
| 57 |
+
python check_credentials.py
|
| 58 |
+
```
|
| 59 |
+
يجب أن ترى: ✅ ملف بيانات الاعتماد صحيح!
|
| 60 |
+
|
| 61 |
+
---
|
| 62 |
+
|
| 63 |
+
## 🎉 الآن جرب الزر!
|
| 64 |
+
- شغل التطبيق
|
| 65 |
+
- اضغط "📤 تصدير إلى Google Docs"
|
| 66 |
+
- ستفتح نافذة متصفح
|
| 67 |
+
- سجل دخول بحساب Google
|
| 68 |
+
- اضغط "Allow" لمنح الصلاحيات
|
| 69 |
+
- ستحصل على رابط المستند!
|
| 70 |
+
|
| 71 |
+
---
|
| 72 |
+
|
| 73 |
+
## 🔧 إذا ظهر تحذير "App isn't verified":
|
| 74 |
+
هذا طبيعي! اضغط:
|
| 75 |
+
1. "Advanced"
|
| 76 |
+
2. "Go to SyncMaster (unsafe)"
|
| 77 |
+
3. "Allow"
|
| 78 |
+
|
| 79 |
+
---
|
| 80 |
+
|
| 81 |
+
## 📞 مازلت تواجه مشاكل؟
|
| 82 |
+
أرسل لي لقطة شاشة من الخطأ وسأساعدك فوراً!
|
GOOGLE_SETUP_SIMPLE.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🚀 إعداد Google Docs - دليل مبسط
|
| 2 |
+
|
| 3 |
+
## ❌ المشكلة الحالية:
|
| 4 |
+
```
|
| 5 |
+
Error 401: invalid_client
|
| 6 |
+
The OAuth client was not found.
|
| 7 |
+
```
|
| 8 |
+
|
| 9 |
+
## ✅ الحل السريع:
|
| 10 |
+
|
| 11 |
+
### الخطوة 1: اذهب إلى Google Cloud Console
|
| 12 |
+
🔗 **الرابط المباشر**: https://console.cloud.google.com/
|
| 13 |
+
|
| 14 |
+
### الخطوة 2: إنشاء مشروع جديد
|
| 15 |
+
1. اضغط على "Select a project" في الأعلى
|
| 16 |
+
2. اضغط على "NEW PROJECT"
|
| 17 |
+
3. اكتب اسم المشروع: `SyncMaster Export`
|
| 18 |
+
4. اضغط "CREATE"
|
| 19 |
+
|
| 20 |
+
### الخطوة 3: تفعيل Google Docs API
|
| 21 |
+
1. في القائمة الجانبية ← "APIs & Services" ← "Library"
|
| 22 |
+
2. ابحث عن: `Google Docs API`
|
| 23 |
+
3. اضغط على النتيجة الأولى
|
| 24 |
+
4. اضغط "ENABLE"
|
| 25 |
+
|
| 26 |
+
### الخطوة 4: إعداد OAuth Consent Screen
|
| 27 |
+
1. اذهب إلى "APIs & Services" ← "OAuth consent screen"
|
| 28 |
+
2. اختر "External"
|
| 29 |
+
3. اضغط "CREATE"
|
| 30 |
+
4. املأ المعلومات المطلوبة:
|
| 31 |
+
- **App name**: `SyncMaster Export`
|
| 32 |
+
- **User support email**: إيميلك
|
| 33 |
+
- **Developer contact information**: إيميلك
|
| 34 |
+
5. اضغط "SAVE AND CONTINUE"
|
| 35 |
+
6. **في صفحة "Scopes"**:
|
| 36 |
+
- **لا تضيف أي Scopes يدوياً**
|
| 37 |
+
- فقط اضغط "SAVE AND CONTINUE" مباشرة
|
| 38 |
+
- (التطبيق سيطلب الصلاحيات تلقائياً عند الاستخدام)
|
| 39 |
+
7. في صفحة "Test users": اضغط "SAVE AND CONTINUE"
|
| 40 |
+
8. في صفحة "Summary": اضغط "BACK TO DASHBOARD"
|
| 41 |
+
|
| 42 |
+
### الخطوة 5: إنشاء OAuth 2.0 Client ID
|
| 43 |
+
1. اذهب إلى "APIs & Services" ← "Credentials"
|
| 44 |
+
2. اضغط "+ CREATE CREDENTIALS" ← "OAuth 2.0 Client ID"
|
| 45 |
+
3. اختر "Desktop application"
|
| 46 |
+
4. اكتب الاسم: `SyncMaster Desktop`
|
| 47 |
+
5. **اترك الحقول فارغة**:
|
| 48 |
+
- **Authorised JavaScript origins**: اتركه فارغ (لا تدخل شيء)
|
| 49 |
+
- **Authorised redirect URIs**: اتركه فارغ (لا تدخل شيء)
|
| 50 |
+
6. اضغط "CREATE"
|
| 51 |
+
|
| 52 |
+
### الخطوة 6: تحميل ملف البيانات
|
| 53 |
+
1. ستظهر نافذة منبثقة مع Client ID و Client Secret
|
| 54 |
+
2. اضغط "DOWNLOAD JSON"
|
| 55 |
+
3. احفظ الملف باسم `credentials.json`
|
| 56 |
+
4. انسخ الملف إلى مجلد التطبيق (نفس مجلد app.py)
|
| 57 |
+
|
| 58 |
+
### الخطوة 7: اختبار التطبيق
|
| 59 |
+
1. احذف ملف `token.json` إذا كان موجوداً
|
| 60 |
+
2. شغل التطبيق
|
| 61 |
+
3. اضغط على زر "📤 تصدير إلى Google Docs"
|
| 62 |
+
4. ستفتح نافذة متصفح للمصادقة
|
| 63 |
+
5. سجل دخول بحساب Google
|
| 64 |
+
6. اضغط "Allow" لمنح الصلاحيات
|
| 65 |
+
|
| 66 |
+
## 🎯 نصائح مهمة:
|
| 67 |
+
- **استخدم نفس حساب Google** الذي أنشأت به المشروع
|
| 68 |
+
- **لا تشارك ملف credentials.json** مع أحد
|
| 69 |
+
- **إذا ظهر تحذير "App isn't verified"** اضغط "Advanced" ثم "Go to SyncMaster Export (unsafe)"
|
| 70 |
+
|
| 71 |
+
## 🔧 استكشاف الأخطاء:
|
| 72 |
+
|
| 73 |
+
### إذا ظهر "invalid_client":
|
| 74 |
+
- تأكد من استبدال ملف `credentials.json` بالملف الحقيقي من Google
|
| 75 |
+
- تأكد من أن الملف في نفس مجلد `app.py`
|
| 76 |
+
|
| 77 |
+
### إذا ظهر "access_denied":
|
| 78 |
+
- تأكد من الضغط على "Allow" في صفحة المصادقة
|
| 79 |
+
- تأكد من تسجيل الدخول بنفس حساب Google الذي أنشأت به المشروع
|
| 80 |
+
|
| 81 |
+
### إذا ظهر "redirect_uri_mismatch":
|
| 82 |
+
- احذف ملف `token.json` وحاول مرة أخرى
|
| 83 |
+
|
| 84 |
+
## 📞 إذا احتجت مساعدة:
|
| 85 |
+
أرسل لي لقطة شاشة من الخطأ وسأساعدك في حله!
|
INTEGRATION_SOLUTION.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SyncMaster - Integrated Setup
|
| 2 |
+
|
| 3 |
+
## 🚀 التشغيل المبسط (HuggingFace Ready)
|
| 4 |
+
|
| 5 |
+
الآن يمكنك تشغيل التطبيق بأمر واحد فقط:
|
| 6 |
+
|
| 7 |
+
```bash
|
| 8 |
+
npm run dev
|
| 9 |
+
```
|
| 10 |
+
|
| 11 |
+
أو
|
| 12 |
+
|
| 13 |
+
```bash
|
| 14 |
+
npm start
|
| 15 |
+
```
|
| 16 |
+
|
| 17 |
+
## 🔧 كيف تم حل المشكلة
|
| 18 |
+
|
| 19 |
+
### المشكلة السابقة:
|
| 20 |
+
- كان يتطلب تشغيل `python recorder_server.py` و `npm run dev` بشكل منفصل
|
| 21 |
+
- غير مناسب للنشر على HuggingFace أو المنصات السحابية
|
| 22 |
+
|
| 23 |
+
### الحل الجديد:
|
| 24 |
+
1. **خادم متكامل**: تم إنشاء `integrated_server.py` الذي يشغل خادم التسجيل تلقائياً
|
| 25 |
+
2. **نقطة دخول موحدة**: ملف `main.py` يبدأ كل شيء معاً
|
| 26 |
+
3. **تكوين ذكي**: يكتشف البيئة تلقائياً (محلي أو سحابي)
|
| 27 |
+
|
| 28 |
+
## 📁 الملفات الجديدة
|
| 29 |
+
|
| 30 |
+
- `integrated_server.py` - يدير خادم التسجيل المدمج
|
| 31 |
+
- `main.py` - نقطة الدخول الرئيسية
|
| 32 |
+
- `app_config.py` - إعدادات التطبيق
|
| 33 |
+
- `startup.py` - مُشغل متقدم للتطوير
|
| 34 |
+
|
| 35 |
+
## 🎯 للاستخدام العادي
|
| 36 |
+
|
| 37 |
+
```bash
|
| 38 |
+
# تشغيل التطبيق (يشمل خادم التسجيل)
|
| 39 |
+
npm run dev
|
| 40 |
+
|
| 41 |
+
# أو استخدام Python مباشرة
|
| 42 |
+
streamlit run main.py
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
## ⚙️ للتطوير المتقدم
|
| 46 |
+
|
| 47 |
+
```bash
|
| 48 |
+
# تشغيل الخوادم بشكل منفصل (للتطوير)
|
| 49 |
+
npm run dev-separate
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
## 🌐 للنشر على HuggingFace
|
| 53 |
+
|
| 54 |
+
فقط ارفع المشروع واستخدم:
|
| 55 |
+
- **Command**: `npm run start`
|
| 56 |
+
- **Port**: `5050`
|
| 57 |
+
|
| 58 |
+
سيتم تشغيل خادم التسجيل تلقائياً في الخلفية!
|
| 59 |
+
|
| 60 |
+
## ✅ اختبار النظام
|
| 61 |
+
|
| 62 |
+
```bash
|
| 63 |
+
python integrated_server.py
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
## 🎉 النتيجة
|
| 67 |
+
|
| 68 |
+
- **✅ تشغيل بأمر واحد فقط**
|
| 69 |
+
- **✅ جاهز للنشر على HuggingFace**
|
| 70 |
+
- **✅ يعمل محلياً وسحابياً**
|
| 71 |
+
- **✅ لا حاجة لتشغيل أوامر متعددة**
|
| 72 |
+
|
| 73 |
+
---
|
| 74 |
+
|
| 75 |
+
المشكلة محلولة! الآن يمكنك استخدام `npm run dev` فقط وسيعمل كل شيء تلقائياً 🎊
|
PERFORMANCE_IMPROVEMENTS.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🚀 تحسينات الأداء - مشكلة الشاشة البيضاء محلولة
|
| 2 |
+
|
| 3 |
+
## 🔍 التحليل والمشكلة:
|
| 4 |
+
كانت المشكلة أن خادم التسجيل يبدأ **بشكل متزامن** عند تحميل الصفحة، مما يسبب:
|
| 5 |
+
- ⏰ تأخير في التحميل (نصف ثانية إلى ثانية)
|
| 6 |
+
- ⚪ شاشة بيضاء أثناء انتظار بدء الخادم
|
| 7 |
+
- 🐌 تجربة مستخدم بطيئة
|
| 8 |
+
|
| 9 |
+
## ✅ الحلول المطبقة:
|
| 10 |
+
|
| 11 |
+
### 1. **تشغيل غير متزامن للخادم**
|
| 12 |
+
```python
|
| 13 |
+
# بدلاً من:
|
| 14 |
+
ensure_recorder_server() # يحجب الواجهة
|
| 15 |
+
|
| 16 |
+
# الآن:
|
| 17 |
+
recorder_thread = threading.Thread(target=start_recorder_async, daemon=True)
|
| 18 |
+
recorder_thread.start() # لا يحجب الواجهة
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
### 2. **تسريع فحص الاستجابة**
|
| 22 |
+
```python
|
| 23 |
+
# قبل: timeout=3 ثوان
|
| 24 |
+
# الآن: timeout=0.5 ثانية
|
| 25 |
+
response = requests.get(url, timeout=0.5)
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
### 3. **تحسين انتظار بدء الخادم**
|
| 29 |
+
```python
|
| 30 |
+
# قبل: sleep(1) × 10 مرات = 10 ثوان
|
| 31 |
+
# الآن: sleep(0.5) × 15 مرة = 7.5 ثانية
|
| 32 |
+
time.sleep(0.5)
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
### 4. **تحسين CSS لمنع الفلاش**
|
| 36 |
+
```css
|
| 37 |
+
.main .block-container {
|
| 38 |
+
animation: fadeIn 0.2s ease-in-out;
|
| 39 |
+
}
|
| 40 |
+
.stSpinner { display: none !important; }
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
### 5. **فحص ذكي للخادم**
|
| 44 |
+
```python
|
| 45 |
+
# فحص سريع أولاً
|
| 46 |
+
if integrated_server.is_server_responding():
|
| 47 |
+
return True # خروج فوري إذا كان يعمل
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
## 📊 النتائج:
|
| 51 |
+
|
| 52 |
+
### قبل التحسين:
|
| 53 |
+
- ⏱️ **تحميل الصفحة**: 1+ ثانية
|
| 54 |
+
- ⚪ **شاشة بيضاء**: نعم
|
| 55 |
+
- 🔄 **تأخير ملحوظ**: نعم
|
| 56 |
+
|
| 57 |
+
### بعد التحسين:
|
| 58 |
+
- ⏱️ **تحميل الصفحة**: 0.008-0.023 ثانية
|
| 59 |
+
- ⚪ **شاشة بيضاء**: لا
|
| 60 |
+
- ⚡ **تحميل فوري**: نعم
|
| 61 |
+
|
| 62 |
+
## 🎯 التحسينات الإضافية:
|
| 63 |
+
|
| 64 |
+
1. **عدم عرض رسائل تحميل غير ضرورية**
|
| 65 |
+
2. **بدء الخادم في الخلفية فقط عند الحاجة**
|
| 66 |
+
3. **تقليل عدد رسائل السجل**
|
| 67 |
+
4. **تحسين CSS للانتقالات السلسة**
|
| 68 |
+
|
| 69 |
+
## 🚀 النتيجة النهائية:
|
| 70 |
+
|
| 71 |
+
✅ **لا مزيد من الشاشة البيضاء**
|
| 72 |
+
✅ **تحميل فوري للمحتوى**
|
| 73 |
+
✅ **تجربة مستخدم سلسة**
|
| 74 |
+
✅ **أداء ممتاز (0.008 ثانية)**
|
| 75 |
+
|
| 76 |
+
**المشكلة محلولة تماماً!** 🎊
|
QUICK_START.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🎯 دليل الإصلاح والتشغيل السريع - SyncMaster Enhanced
|
| 2 |
+
# Quick Fix and Startup Guide - SyncMaster Enhanced
|
| 3 |
+
|
| 4 |
+
## ✅ النظام جاهز للعمل! / System Ready!
|
| 5 |
+
|
| 6 |
+
تم اختبار جميع المكونات بنجاح ✅ All components tested successfully
|
| 7 |
+
|
| 8 |
+
## 🚀 طرق التشغيل / Startup Methods
|
| 9 |
+
|
| 10 |
+
### 1. التشغيل التلقائي المتقدم / Advanced Auto-Start (موصى به / Recommended)
|
| 11 |
+
```bash
|
| 12 |
+
python start_debug.py
|
| 13 |
+
```
|
| 14 |
+
**المزايا / Benefits:**
|
| 15 |
+
- فحص تلقائي للمشاكل / Automatic problem detection
|
| 16 |
+
- إصلاح تضارب المنافذ / Port conflict resolution
|
| 17 |
+
- رسائل خطأ واضحة / Clear error messages
|
| 18 |
+
- تشغيل آمن / Safe startup
|
| 19 |
+
|
| 20 |
+
### 2. التشغيل اليدوي / Manual Startup
|
| 21 |
+
```bash
|
| 22 |
+
# النافذة الأولى / First Terminal
|
| 23 |
+
python recorder_server.py
|
| 24 |
+
|
| 25 |
+
# النافذة الثانية / Second Terminal
|
| 26 |
+
streamlit run app.py --server.port 8501
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
### 3. التشغيل السريع / Quick Start (Windows)
|
| 30 |
+
```bash
|
| 31 |
+
start_enhanced.bat
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
## 🌐 الروابط / URLs
|
| 35 |
+
|
| 36 |
+
بعد التشغيل الناجح / After successful startup:
|
| 37 |
+
|
| 38 |
+
- **🎙️ واجهة التسجيل / Recording Interface**: http://localhost:5001
|
| 39 |
+
- **💻 التطبيق الرئيسي / Main Application**: http://localhost:8501
|
| 40 |
+
- **🔄 فحص حالة الخادم / Server Status**: http://localhost:5001/record
|
| 41 |
+
|
| 42 |
+
## 📋 خطوات الاستخدام / Usage Steps
|
| 43 |
+
|
| 44 |
+
### للطلاب الجدد / For New Users:
|
| 45 |
+
|
| 46 |
+
#### 1. إعداد اللغة / Language Setup
|
| 47 |
+
- اختر اللغة المفضلة (عربي/English)
|
| 48 |
+
- فعّل الترجمة التلقائية
|
| 49 |
+
- اختر اللغة المستهدفة
|
| 50 |
+
|
| 51 |
+
#### 2. التسجيل / Recording
|
| 52 |
+
- اذهب لتبويب "🎙️ Record Audio"
|
| 53 |
+
- اضغط "Start Recording" / "بدء التسجيل"
|
| 54 |
+
- تحدث بوضوح
|
| 55 |
+
- استخدم "Mark Important" للنقاط المهمة
|
| 56 |
+
- اضغط "Stop" عند الانتهاء
|
| 57 |
+
|
| 58 |
+
#### 3. المعالجة / Processing
|
| 59 |
+
- اضغط "Extract Text" / "استخراج النص"
|
| 60 |
+
- انتظر المعالجة (قد تستغرق دقائق)
|
| 61 |
+
- راجع النص الأصلي والمترجم
|
| 62 |
+
|
| 63 |
+
#### 4. الحفظ / Saving
|
| 64 |
+
- انسخ النص المطلوب
|
| 65 |
+
- احفظ ملف JSON للمراجعة لاحقاً
|
| 66 |
+
|
| 67 |
+
## 🔧 استكشاف الأخطاء / Troubleshooting
|
| 68 |
+
|
| 69 |
+
### المشكلة الأكثر شيوعاً / Most Common Issue:
|
| 70 |
+
```
|
| 71 |
+
Error: Failed to fetch
|
| 72 |
+
POST http://localhost:5001/record net::ERR_CONNECTION_REFUSED
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
### الحل السريع / Quick Fix:
|
| 76 |
+
```bash
|
| 77 |
+
# 1. أوقف جميع العمليات / Stop all processes
|
| 78 |
+
taskkill /f /im python.exe
|
| 79 |
+
|
| 80 |
+
# 2. شغّل الاختبار / Run test
|
| 81 |
+
python test_system.py
|
| 82 |
+
|
| 83 |
+
# 3. شغّل النظام / Start system
|
| 84 |
+
python start_debug.py
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
### إذا لم يعمل / If Still Not Working:
|
| 88 |
+
```bash
|
| 89 |
+
# فحص المنافذ / Check ports
|
| 90 |
+
netstat -an | findstr :5001
|
| 91 |
+
netstat -an | findstr :8501
|
| 92 |
+
|
| 93 |
+
# إعادة تثبيت التبعيات / Reinstall dependencies
|
| 94 |
+
pip install --upgrade -r requirements.txt
|
| 95 |
+
```
|
| 96 |
+
|
| 97 |
+
## 💡 نصائح مهمة / Important Tips
|
| 98 |
+
|
| 99 |
+
### للحصول على أفضل النتائج / For Best Results:
|
| 100 |
+
|
| 101 |
+
#### جودة التسجيل / Recording Quality:
|
| 102 |
+
- استخدم سماعة رأس بميكروفون
|
| 103 |
+
- اجلس في مكان هادئ
|
| 104 |
+
- تحدث بوضوح وبطء نسبي
|
| 105 |
+
- تجنب الضوضاء الخلفية
|
| 106 |
+
|
| 107 |
+
#### إعدادات الترجمة / Translation Settings:
|
| 108 |
+
- **للطلاب العرب**: فعّل الترجمة للإنجليزية لفهم المصطلحات التقنية
|
| 109 |
+
- **للطلاب الدوليين**: استخدم الترجمة للغتك الأم
|
| 110 |
+
- **للمحاضرات المختلطة**: راجع النص بكلا اللغتين
|
| 111 |
+
|
| 112 |
+
#### استخدام العلامات / Using Markers:
|
| 113 |
+
- ضع علامة عند المفاهيم الجديدة
|
| 114 |
+
- اعلم النقاط المهمة للامتحان
|
| 115 |
+
- استخدم العلامات للتنظيم
|
| 116 |
+
|
| 117 |
+
## 📱 متطلبات النظام / System Requirements
|
| 118 |
+
|
| 119 |
+
### الحد الأدنى / Minimum:
|
| 120 |
+
- Python 3.8+
|
| 121 |
+
- 4 GB RAM
|
| 122 |
+
- اتصال إنترنت للترجمة
|
| 123 |
+
- مساحة 1 GB على القرص الصلب
|
| 124 |
+
|
| 125 |
+
### الموصى به / Recommended:
|
| 126 |
+
- Python 3.10+
|
| 127 |
+
- 8 GB RAM
|
| 128 |
+
- اتصال إنترنت سريع
|
| 129 |
+
- SSD للتخزين
|
| 130 |
+
- ميكروفون عالي الجودة
|
| 131 |
+
|
| 132 |
+
## 🌟 ميزات متقدمة / Advanced Features
|
| 133 |
+
|
| 134 |
+
### اختصارات لوحة المفاتيح / Keyboard Shortcuts:
|
| 135 |
+
- **Space**: بدء/إيقاف التسجيل
|
| 136 |
+
- **M**: وضع علامة مهمة
|
| 137 |
+
- **P**: إيقاف مؤقت/استئناف
|
| 138 |
+
- **R**: إعادة تسجيل
|
| 139 |
+
|
| 140 |
+
### واجهة برمجة التطبيقات / API Features:
|
| 141 |
+
- ترجمة نصوص مستقلة
|
| 142 |
+
- معالجة مجمعة للملفات
|
| 143 |
+
- كشف اللغة التلقائي
|
| 144 |
+
- تخصيص إعدادات الصوت
|
| 145 |
+
|
| 146 |
+
## 📞 الدعم التقني / Technical Support
|
| 147 |
+
|
| 148 |
+
### أدوات التشخيص / Diagnostic Tools:
|
| 149 |
+
```bash
|
| 150 |
+
# اختبار شامل / Complete test
|
| 151 |
+
python test_system.py
|
| 152 |
+
|
| 153 |
+
# فحص الاتصال / Connection test
|
| 154 |
+
python -c "import requests; print(requests.get('http://localhost:5001/record').status_code)"
|
| 155 |
+
|
| 156 |
+
# اختبار الترجمة / Translation test
|
| 157 |
+
python -c "from translator import AITranslator; t=AITranslator(); print(t.translate_text('Hello', 'ar'))"
|
| 158 |
+
```
|
| 159 |
+
|
| 160 |
+
### ملفات السجل / Log Files:
|
| 161 |
+
- تحقق من console المتصفح (F12)
|
| 162 |
+
- راجع سجلات الطرفية
|
| 163 |
+
- ابحث عن ملفات tmp*.json
|
| 164 |
+
|
| 165 |
+
## 🎓 للمدرسين والمحاضرين / For Teachers and Lecturers
|
| 166 |
+
|
| 167 |
+
### إعدادات الفصل / Classroom Setup:
|
| 168 |
+
- تأكد من إذن التسجيل
|
| 169 |
+
- وضح للطلاب كيفية الاستخدام
|
| 170 |
+
- اقترح جلسات تدريبية
|
| 171 |
+
|
| 172 |
+
### نصائح للمحاضرات / Lecture Tips:
|
| 173 |
+
- تحدث بوضوح
|
| 174 |
+
- اكرر المصطلحات المهمة
|
| 175 |
+
- استخدم فترات صمت قصيرة
|
| 176 |
+
- اشرح بعدة لغات إذا أمكن
|
| 177 |
+
|
| 178 |
+
---
|
| 179 |
+
|
| 180 |
+
## 🎉 مبروك! / Congratulations!
|
| 181 |
+
|
| 182 |
+
**النظام جاهز للاستخدام! / System is ready to use!**
|
| 183 |
+
|
| 184 |
+
```bash
|
| 185 |
+
# للبدء الآن / To start now:
|
| 186 |
+
python start_debug.py
|
| 187 |
+
```
|
| 188 |
+
|
| 189 |
+
**استمتع بتجربة تعليمية محسنة مع SyncMaster! 🚀**
|
| 190 |
+
**Enjoy an enhanced learning experience with SyncMaster! 🚀**
|
| 191 |
+
|
| 192 |
+
---
|
| 193 |
+
|
| 194 |
+
### 📋 Checklist
|
| 195 |
+
|
| 196 |
+
- ✅ Python مثبت / Python installed
|
| 197 |
+
- ✅ التبعيات مثبتة / Dependencies installed
|
| 198 |
+
- ✅ مفتاح API مُعد / API key configured
|
| 199 |
+
- ✅ اختبار النظام نجح / System test passed
|
| 200 |
+
- ✅ جاهز للاستخدام / Ready to use
|
| 201 |
+
|
| 202 |
+
**🎯 التالي: python start_debug.py**
|
README.md
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: SyncMaster Enhanced
|
| 3 |
+
emoji: 🚀
|
| 4 |
+
colorFrom: red
|
| 5 |
+
colorTo: red
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 8501
|
| 8 |
+
tags:
|
| 9 |
+
- streamlit
|
| 10 |
+
- ai-translation
|
| 11 |
+
- speech-to-text
|
| 12 |
+
- multilingual
|
| 13 |
+
- education
|
| 14 |
+
pinned: false
|
| 15 |
+
short_description: AI-powered audio transcription
|
| 16 |
+
license: mit
|
| 17 |
+
---
|
| 18 |
+
|
| 19 |
+
# SyncMaster Enhanced - AI-Powered Audio Transcription & Translation
|
| 20 |
+
|
| 21 |
+
> **🌟 New: Enhanced with AI Translation Support for International Students**
|
| 22 |
+
> **جديد: محسن مع دعم الترجمة بالذكاء الاصطناعي للطلاب الدوليين**
|
| 23 |
+
|
| 24 |
+
SyncMaster is an intelligent audio-text synchronization platform specifically designed for international students in universities. It provides real-time audio recording, AI-powered transcription, and automatic translation to help students better understand and review their lectures.
|
| 25 |
+
|
| 26 |
+
## ✨ Key Features
|
| 27 |
+
|
| 28 |
+
### 🌐 Multi-Language Support
|
| 29 |
+
- **Full Arabic Interface**: Complete Arabic UI for better accessibility
|
| 30 |
+
- **AI-Powered Translation**: Automatic translation to Arabic, English, French, and Spanish
|
| 31 |
+
- **Language Detection**: Automatically detects the source language
|
| 32 |
+
- **Academic Context**: Specialized translation for academic content
|
| 33 |
+
|
| 34 |
+
### 🎙️ Enhanced Recording
|
| 35 |
+
- **Browser-based Recording**: Record directly from your web browser
|
| 36 |
+
- **Real-time Audio Visualization**: Visual feedback during recording
|
| 37 |
+
- **Important Markers**: Mark important points during lectures
|
| 38 |
+
- **Pause/Resume**: Full control over recording sessions
|
| 39 |
+
|
| 40 |
+
### 🤖 AI Technology
|
| 41 |
+
- **Gemini AI Integration**: Accurate transcription using Google's Gemini AI
|
| 42 |
+
- **Advanced Translation**: Context-aware translation for educational content
|
| 43 |
+
- **Parallel Processing**: Fast and efficient audio processing
|
| 44 |
+
|
| 45 |
+
### 📱 Student-Friendly Features
|
| 46 |
+
- **Responsive Design**: Works on desktop, tablet, and mobile
|
| 47 |
+
- **Keyboard Shortcuts**: Quick access to common functions
|
| 48 |
+
- **Accessibility**: Screen reader support and RTL language support
|
| 49 |
+
- **Offline Capability**: Process recordings without constant internet
|
| 50 |
+
|
| 51 |
+
## 🚀 Quick Start
|
| 52 |
+
|
| 53 |
+
### For International Students:
|
| 54 |
+
|
| 55 |
+
1. **Setup**:
|
| 56 |
+
```bash
|
| 57 |
+
# Clone or download the project
|
| 58 |
+
# Install Python 3.8+
|
| 59 |
+
python setup_enhanced.py
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
2. **Run**:
|
| 63 |
+
```bash
|
| 64 |
+
# Windows
|
| 65 |
+
start_enhanced.bat
|
| 66 |
+
|
| 67 |
+
# Linux/Mac
|
| 68 |
+
python setup_enhanced.py
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
3. **Configure**:
|
| 72 |
+
- Add your Gemini API key to `.env` file
|
| 73 |
+
- Choose your preferred language (Arabic/English)
|
| 74 |
+
- Enable translation and select target language
|
| 75 |
+
|
| 76 |
+
### API Key Setup:
|
| 77 |
+
1. Get a free Gemini API key from [Google AI Studio](https://makersuite.google.com/app/apikey)
|
| 78 |
+
2. Add it to your `.env` file:
|
| 79 |
+
```
|
| 80 |
+
GEMINI_API_KEY=your_api_key_here
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
## 📖 Usage Guide
|
| 84 |
+
|
| 85 |
+
### Recording Lectures:
|
| 86 |
+
1. Go to the **Record Audio** tab
|
| 87 |
+
2. Click **Start Recording**
|
| 88 |
+
3. Use **Mark Important** for key points
|
| 89 |
+
4. Click **Stop** when finished
|
| 90 |
+
5. Click **Extract Text** to process
|
| 91 |
+
|
| 92 |
+
### Translation:
|
| 93 |
+
1. Enable translation in settings
|
| 94 |
+
2. Select target language
|
| 95 |
+
3. Process your audio
|
| 96 |
+
4. Review both original and translated text
|
| 97 |
+
|
| 98 |
+
### Export Options:
|
| 99 |
+
- Copy text for notes
|
| 100 |
+
- Save as files for later review
|
| 101 |
+
- Generate synchronized videos (coming soon)
|
| 102 |
+
|
| 103 |
+
## 🎓 For Students
|
| 104 |
+
|
| 105 |
+
### Arabic Students (للطلاب العرب):
|
| 106 |
+
- استخدم الواجهة العربية لسهولة الاستخدام
|
| 107 |
+
- فعّل الترجمة للإنجليزية لفهم المصطلحات التقنية
|
| 108 |
+
- ضع علامات على المفاهيم الجديدة أثناء المحاضرة
|
| 109 |
+
|
| 110 |
+
### International Students:
|
| 111 |
+
- Use translation to your native language for better understanding
|
| 112 |
+
- Mark important concepts during lectures
|
| 113 |
+
- Review both original and translated text together
|
| 114 |
+
|
| 115 |
+
## ⌨️ Keyboard Shortcuts
|
| 116 |
+
- **Space**: Start/Stop recording
|
| 117 |
+
- **M**: Mark important point
|
| 118 |
+
- **P**: Pause/Resume
|
| 119 |
+
- **R**: Re-record
|
| 120 |
+
|
| 121 |
+
## 🔧 Technical Requirements
|
| 122 |
+
|
| 123 |
+
### System Requirements:
|
| 124 |
+
- Python 3.8 or higher
|
| 125 |
+
- Modern web browser (Chrome, Firefox, Safari, Edge)
|
| 126 |
+
- Microphone access for recording
|
| 127 |
+
- Internet connection for AI processing
|
| 128 |
+
|
| 129 |
+
### Dependencies:
|
| 130 |
+
- Streamlit (Web interface)
|
| 131 |
+
- Google Generative AI (Transcription & Translation)
|
| 132 |
+
- Flask (Recording server)
|
| 133 |
+
- LibROSA (Audio processing)
|
| 134 |
+
|
| 135 |
+
## 📱 Browser Compatibility
|
| 136 |
+
|
| 137 |
+
| Browser | Recording | Translation | UI |
|
| 138 |
+
|---------|-----------|-------------|----|
|
| 139 |
+
| Chrome | ✅ | ✅ | ✅ |
|
| 140 |
+
| Firefox | ✅ | ✅ | ✅ |
|
| 141 |
+
| Safari | ✅ | ✅ | ✅ |
|
| 142 |
+
| Edge | ✅ | ✅ | ✅ |
|
| 143 |
+
|
| 144 |
+
## 🛠️ Troubleshooting
|
| 145 |
+
|
| 146 |
+
### Common Issues:
|
| 147 |
+
|
| 148 |
+
**Microphone not working:**
|
| 149 |
+
- Grant microphone permission to your browser
|
| 150 |
+
- Check system audio settings
|
| 151 |
+
- Try a different browser
|
| 152 |
+
|
| 153 |
+
**Translation errors:**
|
| 154 |
+
- Check internet connection
|
| 155 |
+
- Verify Gemini API key
|
| 156 |
+
- Try processing again
|
| 157 |
+
|
| 158 |
+
**Poor transcription quality:**
|
| 159 |
+
- Ensure clear audio recording
|
| 160 |
+
- Reduce background noise
|
| 161 |
+
- Speak clearly and at moderate pace
|
| 162 |
+
|
| 163 |
+
## 🔮 Roadmap
|
| 164 |
+
|
| 165 |
+
### Coming Soon:
|
| 166 |
+
- **Smart Content Analysis**: Automatic extraction of key concepts
|
| 167 |
+
- **Study Cards**: Generate flashcards from lectures
|
| 168 |
+
- **Platform Integration**: Connect with Moodle, Canvas, etc.
|
| 169 |
+
- **Collaborative Features**: Share recordings with classmates
|
| 170 |
+
- **Advanced Analytics**: Learning progress tracking
|
| 171 |
+
|
| 172 |
+
## 📚 Documentation
|
| 173 |
+
|
| 174 |
+
- [**Arabic Guide**](README_AR.md) - دليل باللغة العربية
|
| 175 |
+
- [**API Documentation**](docs/api.md) - Technical API reference
|
| 176 |
+
- [**Troubleshooting**](docs/troubleshooting.md) - Detailed problem solving
|
| 177 |
+
|
| 178 |
+
## 🤝 Contributing
|
| 179 |
+
|
| 180 |
+
We welcome contributions from the international student community:
|
| 181 |
+
|
| 182 |
+
1. Fork the repository
|
| 183 |
+
2. Create a feature branch
|
| 184 |
+
3. Add your improvements
|
| 185 |
+
4. Submit a pull request
|
| 186 |
+
|
| 187 |
+
### Areas for Contribution:
|
| 188 |
+
- Additional language support
|
| 189 |
+
- UI improvements
|
| 190 |
+
- Mobile optimization
|
| 191 |
+
- Documentation translation
|
| 192 |
+
|
| 193 |
+
## 📄 License
|
| 194 |
+
|
| 195 |
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
| 196 |
+
|
| 197 |
+
## 🙏 Acknowledgments
|
| 198 |
+
|
| 199 |
+
- Google Gemini AI for transcription and translation
|
| 200 |
+
- Streamlit team for the amazing web framework
|
| 201 |
+
- International student community for feedback and testing
|
| 202 |
+
|
| 203 |
+
## 📞 Support
|
| 204 |
+
|
| 205 |
+
For technical support or questions:
|
| 206 |
+
- Check the browser console (F12) for error details
|
| 207 |
+
- Review log files in the application directory
|
| 208 |
+
- Ensure all dependencies are up to date
|
| 209 |
+
|
| 210 |
+
---
|
| 211 |
+
|
| 212 |
+
**Made with ❤️ for international students worldwide**
|
| 213 |
+
**صُنع بـ ❤️ للطلاب الدوليين حول العالم**
|
| 214 |
+
|
| 215 |
+
---
|
| 216 |
+
|
| 217 |
+
### Quick Links:
|
| 218 |
+
- 🚀 [Quick Start Guide](docs/quickstart.md)
|
| 219 |
+
- 🌐 [Arabic Documentation](README_AR.md)
|
| 220 |
+
- 🎓 [Student Guide](docs/student-guide.md)
|
| 221 |
+
- 🔧 [Technical Setup](docs/technical-setup.md)
|
README_AR.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SyncMaster - دليل المستخدم للطلاب الأجانب
|
| 2 |
+
|
| 3 |
+
## 🎯 نظرة عامة
|
| 4 |
+
SyncMaster هو تطبيق ذكي مطور خصيصاً للطلاب الأجانب في الجامعات لتسجيل المحاضرات وتحويلها إلى نص مكتوب مع ترجمة فورية باستخدام الذكاء الاصطناعي.
|
| 5 |
+
|
| 6 |
+
## ✨ الميزات الجديدة
|
| 7 |
+
|
| 8 |
+
### 🌐 دعم متعدد اللغات
|
| 9 |
+
- **واجهة عربية كاملة**: تم تطوير واجهة باللغة العربية لتسهيل الاستخدام
|
| 10 |
+
- **ترجمة فورية**: ترجمة النص المنسوخ إلى العربية والإنجليزية والفرنسية والإسبانية
|
| 11 |
+
- **كشف اللغة التلقائي**: يتعرف النظام على لغة المحاضرة تلقائياً
|
| 12 |
+
|
| 13 |
+
### 🎙️ ميزات التسجيل المحسنة
|
| 14 |
+
- **تسجيل مباشر**: تسجيل المحاضرات مباشرة من المتصفح
|
| 15 |
+
- **علامات مهمة**: وضع علامات على النقاط المهمة أثناء التسجيل
|
| 16 |
+
- **مؤشر مستوى الصوت**: عرض مرئي لمستوى الصوت
|
| 17 |
+
- **إيقاف مؤقت واستئناف**: تحكم كامل في التسجيل
|
| 18 |
+
|
| 19 |
+
### 🤖 ذكاء اصطناعي متطور
|
| 20 |
+
- **نسخ دقيق**: استخدام Gemini AI لنسخ دقيق للمحاضرات
|
| 21 |
+
- **ترجمة محسنة**: ترجمة متخصصة للمحتوى الأكاديمي
|
| 22 |
+
- **معالجة متوازية**: معالجة سريعة وفعالة
|
| 23 |
+
|
| 24 |
+
## 🚀 كيفية الاستخدام
|
| 25 |
+
|
| 26 |
+
### الخطوة 1: إعداد اللغة
|
| 27 |
+
1. اختر لغة الواجهة من القائمة العلوية (العربية/English)
|
| 28 |
+
2. فعّل الترجمة التلقائية
|
| 29 |
+
3. اختر اللغة المستهدفة للترجمة
|
| 30 |
+
|
| 31 |
+
### الخطوة 2: التسجيل
|
| 32 |
+
1. اضغط على تبويب "🎙️ Record Audio"
|
| 33 |
+
2. اضغط "Start Recording" لبدء التسجيل
|
| 34 |
+
3. استخدم "Mark Important" لوضع علامات على النقاط المهمة
|
| 35 |
+
4. اضغط "Stop" لإنهاء التسجيل
|
| 36 |
+
|
| 37 |
+
### الخطوة 3: المعالجة والترجمة
|
| 38 |
+
1. اضغط "Extract Text" لبدء المعالجة
|
| 39 |
+
2. انتظر حتى يكتمل النسخ والترجمة
|
| 40 |
+
3. راجع النص الأصلي والمترجم
|
| 41 |
+
|
| 42 |
+
### الخطوة 4: التصدير
|
| 43 |
+
1. احفظ النتائج أو انسخها
|
| 44 |
+
2. استخدم الملف المحفوظ للمراجعة لاحقاً
|
| 45 |
+
|
| 46 |
+
## ⌨️ اختصارات لوحة المفاتيح
|
| 47 |
+
- **Space**: بدء/إيقاف التسجيل
|
| 48 |
+
- **M**: وضع علامة مهمة
|
| 49 |
+
- **P**: إيقاف مؤقت/استئناف
|
| 50 |
+
- **R**: إعادة تسجيل
|
| 51 |
+
|
| 52 |
+
## 📱 نصائح للطلاب الأجانب
|
| 53 |
+
|
| 54 |
+
### للطلاب العرب:
|
| 55 |
+
- استخدم الواجهة العربية للسهولة
|
| 56 |
+
- فعّل الترجمة للإنجليزية لفهم المصطلحات التقنية
|
| 57 |
+
- ضع علامات على المفاهيم الجديدة
|
| 58 |
+
|
| 59 |
+
### للطلاب الدوليين:
|
| 60 |
+
- استخدم الترجمة إلى لغتك الأم للفهم الأفضل
|
| 61 |
+
- اعتمد على العلامات المهمة للمراجعة السريعة
|
| 62 |
+
- راجع النص المترجم والأصلي معاً
|
| 63 |
+
|
| 64 |
+
## 🔧 إعدادات متقدمة
|
| 65 |
+
|
| 66 |
+
### جودة التسجيل:
|
| 67 |
+
- **عالية**: للمحاضرات المهمة (320 kbps)
|
| 68 |
+
- **متوسطة**: للاستخدام العادي (192 kbps)
|
| 69 |
+
- **منخفضة**: لتوفير المساحة (128 kbps)
|
| 70 |
+
|
| 71 |
+
### إعدادات الترجمة:
|
| 72 |
+
- **Arabic**: للطلاب العرب
|
| 73 |
+
- **English**: للمحتوى الدولي
|
| 74 |
+
- **French**: للطلاب الفرنكوفونيين
|
| 75 |
+
- **Spanish**: للطلاب الناطقين بالإسبانية
|
| 76 |
+
|
| 77 |
+
## 🛠️ استكشاف الأخطاء
|
| 78 |
+
|
| 79 |
+
### مشاكل الميكروفون:
|
| 80 |
+
1. تأكد من إعطاء إذن الميكروفون للمتصفح
|
| 81 |
+
2. تحقق من إعدادات الصوت في النظام
|
| 82 |
+
3. جرب متصفح آخر إذا لزم الأمر
|
| 83 |
+
|
| 84 |
+
### مشاكل الترجمة:
|
| 85 |
+
1. تأكد من اتصال الإنترنت
|
| 86 |
+
2. تحقق من صحة مفتاح API
|
| 87 |
+
3. جرب إعادة المعالجة
|
| 88 |
+
|
| 89 |
+
### مشاكل في النسخ:
|
| 90 |
+
1. تأكد من وضوح الصوت
|
| 91 |
+
2. قلل الضوضاء في الخلفية
|
| 92 |
+
3. تحدث بوضوح وبطء نسبياً
|
| 93 |
+
|
| 94 |
+
## 📞 الدعم التقني
|
| 95 |
+
|
| 96 |
+
### الحصول على المساعدة:
|
| 97 |
+
- تحقق من console المتصفح (F12) للأخطاء
|
| 98 |
+
- راجع ملفات السجل في مجلد التطبيق
|
| 99 |
+
- تأكد من تحديث جميع المكتبات
|
| 100 |
+
|
| 101 |
+
### نصائح للأداء الأفضل:
|
| 102 |
+
- استخدم Chrome أو Firefox للتوافق الأفضل
|
| 103 |
+
- أغلق التطبيقات الأخرى أثناء التسجيل
|
| 104 |
+
- تأكد من مساحة كافية على القرص الصلب
|
| 105 |
+
|
| 106 |
+
## 🎓 نصائح أكاديمية
|
| 107 |
+
|
| 108 |
+
### للمحاضرات:
|
| 109 |
+
- اجلس في مقدمة القاعة للصوت الأوضح
|
| 110 |
+
- استخدم علامات المحاضر المهمة كدليل
|
| 111 |
+
- راجع الترجمة مع زملاء الدراسة
|
| 112 |
+
|
| 113 |
+
### للمذاكرة:
|
| 114 |
+
- استخدم النص المترجم للمراجعة السريعة
|
| 115 |
+
- ابحث عن المفاهيم المترجمة في مصادر إضافية
|
| 116 |
+
- اربط النص الأصلي بالترجمة لتحسين اللغة
|
| 117 |
+
|
| 118 |
+
## 🔮 ميزات قادمة
|
| 119 |
+
|
| 120 |
+
### التحديثات المخططة:
|
| 121 |
+
- **تحليل المحتوى**: استخراج النقاط الرئيسية تلقائياً
|
| 122 |
+
- **بطاقات المراجعة**: إنشاء بطاقات دراسة من المحاضرات
|
| 123 |
+
- **التكامل مع المنصات**: ربط مع Moodle وCanvas
|
| 124 |
+
- **المشاركة التعاونية**: مشاركة المحاضرات مع الزملاء
|
| 125 |
+
|
| 126 |
+
---
|
| 127 |
+
|
| 128 |
+
## 📄 إخلاء المسؤولية
|
| 129 |
+
|
| 130 |
+
هذا التطبيق مخصص للاستخدام التعليمي. تأكد من الحصول على إذن المحاضر قبل تسجيل المحاضرات. النسخ والترجمة قد يحتويان على أخطاء، لذا راجعهما دائماً.
|
| 131 |
+
|
| 132 |
+
---
|
| 133 |
+
|
| 134 |
+
**نتمنى لك تجربة تعليمية ممتازة مع SyncMaster! 🎓✨**
|
SOLUTION_SUMMARY.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🎉 تم حل المشكلة بنجاح!
|
| 2 |
+
|
| 3 |
+
## ✅ ملخص الحل
|
| 4 |
+
|
| 5 |
+
تم حل مشكلة "system offline" في ميزة Lecture Recorder بنجاح. الآن يمكنك تشغيل التطبيق بأمر واحد فقط:
|
| 6 |
+
|
| 7 |
+
```bash
|
| 8 |
+
npm run dev
|
| 9 |
+
```
|
| 10 |
+
|
| 11 |
+
## 🔧 التغييرات التي تمت
|
| 12 |
+
|
| 13 |
+
### 1. ملفات جديدة تم إنشاؤها:
|
| 14 |
+
- `integrated_server.py` - خادم متكامل للتسجيل
|
| 15 |
+
- `main.py` - نقطة دخول بسيطة ومدمجة
|
| 16 |
+
- `app_config.py` - إعدادات التطبيق
|
| 17 |
+
- `startup.py` - مُشغل متقدم للتطوير
|
| 18 |
+
|
| 19 |
+
### 2. ملفات تم تعديلها:
|
| 20 |
+
- `app.py` - إضافة استيراد الخادم المدمج
|
| 21 |
+
- `package.json` - تحديث أوامر التشغيل
|
| 22 |
+
|
| 23 |
+
## 🚀 كيفية الاستخدام
|
| 24 |
+
|
| 25 |
+
### للاستخدام العادي:
|
| 26 |
+
```bash
|
| 27 |
+
npm run dev
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
### للنشر على HuggingFace:
|
| 31 |
+
```bash
|
| 32 |
+
npm start
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
### للتطوير المتقدم (خوادم منفصلة):
|
| 36 |
+
```bash
|
| 37 |
+
npm run dev-separate
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
## ✨ المميزات الجديدة
|
| 41 |
+
|
| 42 |
+
1. **🎯 تشغيل موحد**: أمر واحد فقط لتشغيل كل شيء
|
| 43 |
+
2. **☁️ جاهز للسحابة**: يعمل تلقائياً على HuggingFace و Railway
|
| 44 |
+
3. **🔧 تكوين ذكي**: يكتشف البيئة ويتكيف معها
|
| 45 |
+
4. **🛡️ معالجة أخطاء محسنة**: تشغيل احتياطي في حالة فشل الطريقة الأولى
|
| 46 |
+
5. **📊 مراقبة الحالة**: فحص تلقائي لحالة الخوادم
|
| 47 |
+
|
| 48 |
+
## 🧪 اختبار النظام
|
| 49 |
+
|
| 50 |
+
تم اختبار النظام وأظهر النتائج التالية:
|
| 51 |
+
- ✅ خادم التسجيل يبدأ تلقائياً
|
| 52 |
+
- ✅ Streamlit يعمل على المنفذ 5050
|
| 53 |
+
- ✅ خادم التسجيل يعمل على المنفذ 5001
|
| 54 |
+
- ✅ التكامل بين الخوادم يعمل بنجاح
|
| 55 |
+
|
| 56 |
+
## 🎊 النتيجة النهائية
|
| 57 |
+
|
| 58 |
+
**المشكلة محلولة تماماً!**
|
| 59 |
+
|
| 60 |
+
لن تحتاج بعد الآن إلى:
|
| 61 |
+
- ❌ تشغيل `python recorder_server.py` منفصلاً
|
| 62 |
+
- ❌ القلق بشأن "system offline"
|
| 63 |
+
- ❌ تشغيل أوامر متعددة
|
| 64 |
+
|
| 65 |
+
فقط استخدم `npm run dev` وسيعمل كل شيء تلقائياً! 🚀
|
| 66 |
+
|
| 67 |
+
---
|
| 68 |
+
|
| 69 |
+
**جاهز للنشر على HuggingFace الآن!** 🌟
|
SUMMARY_FIX_REPORT.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# حل مشكلة زر التلخيص - تقرير الإصلاح النهائي 🎉
|
| 2 |
+
|
| 3 |
+
## 📋 ملخص المشكلة
|
| 4 |
+
كان زر "Generate Smart Lecture Summary" لا يعمل في تلخيص النص المستخرج من الذكاء الاصطناعي بعد جلبه من الصوت، مع ظهور خطأ CORS:
|
| 5 |
+
|
| 6 |
+
```
|
| 7 |
+
Access to fetch at 'http://localhost:5001/summarize' from origin 'http://localhost:5054' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: The 'Access-Control-Allow-Origin' header contains multiple values '*, *', but only one is allowed.
|
| 8 |
+
```
|
| 9 |
+
|
| 10 |
+
## 🔍 التشخيص المنجز
|
| 11 |
+
تم إنشاء نظام تشخيص شامل كشف عن:
|
| 12 |
+
|
| 13 |
+
### 1. مشكلة CORS الرئيسية ❌
|
| 14 |
+
- **المشكلة**: الخادم يرسل `'*, *'` بدلاً من `'*'`
|
| 15 |
+
- **السبب**: تكرار إعدادات CORS - مرة من `flask-cors` ومرة يدوياً في كل endpoint
|
| 16 |
+
- **النتيجة**: تكرار header `Access-Control-Allow-Origin`
|
| 17 |
+
|
| 18 |
+
### 2. مكتبة مفقودة ❌
|
| 19 |
+
- **المشكلة**: `google-generativeai` غير مثبتة
|
| 20 |
+
- **التأثير**: فشل في وظيفة التلخيص
|
| 21 |
+
|
| 22 |
+
## ✅ الحلول المطبقة
|
| 23 |
+
|
| 24 |
+
### 1. إصلاح مشكلة CORS
|
| 25 |
+
#### أ. تبسيط إعداد CORS في `recorder_server.py`:
|
| 26 |
+
```python
|
| 27 |
+
# قبل الإصلاح - إعداد معقد
|
| 28 |
+
CORS(app, resources={
|
| 29 |
+
r"/record": {"origins": "*"},
|
| 30 |
+
r"/translate": {"origins": "*"},
|
| 31 |
+
r"/languages": {"origins": "*"},
|
| 32 |
+
r"/ui-translations/*": {"origins": "*"},
|
| 33 |
+
r"/notes": {"origins": "*"},
|
| 34 |
+
r"/notes/*": {"origins": "*"},
|
| 35 |
+
r"/summarize": {"origins": "*"}
|
| 36 |
+
})
|
| 37 |
+
|
| 38 |
+
# بعد الإصلاح - إعداد مبسط وصحيح
|
| 39 |
+
CORS(app,
|
| 40 |
+
origins="*",
|
| 41 |
+
methods=['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
| 42 |
+
allow_headers=['Content-Type', 'Authorization']
|
| 43 |
+
)
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
#### ب. إزالة الإعدادات اليدوية المكررة:
|
| 47 |
+
```python
|
| 48 |
+
# قبل الإصلاح - إعداد يدوي مكرر
|
| 49 |
+
if request.method == 'OPTIONS':
|
| 50 |
+
headers = {
|
| 51 |
+
'Access-Control-Allow-Origin': '*',
|
| 52 |
+
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
| 53 |
+
'Access-Control-Allow-Headers': 'Content-Type',
|
| 54 |
+
}
|
| 55 |
+
return ('', 204, headers)
|
| 56 |
+
|
| 57 |
+
# بعد الإصلاح - تبسيط
|
| 58 |
+
if request.method == 'OPTIONS':
|
| 59 |
+
return '', 204 # flask-cors ستتولى الأمر
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
### 2. تثبيت المكتبات المفقودة
|
| 63 |
+
```bash
|
| 64 |
+
pip install google-generativeai
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
### 3. تحسين معالجة الأخطاء في JavaScript
|
| 68 |
+
تم تحديث دالة `generateSummary()` في `templates/recorder.html`:
|
| 69 |
+
- رسائل خطأ محددة باللغة العربية
|
| 70 |
+
- معالجة أفضل لتنسيقات الاستجابة المختلفة
|
| 71 |
+
- تشخيص أوضح للمشاكل
|
| 72 |
+
|
| 73 |
+
### 4. تحسين دالة عرض النتائج
|
| 74 |
+
تم تحديث `displaySummaryResults()` للتعامل مع:
|
| 75 |
+
- تنسيقات مختلفة للاستجابة (نص أو كائن)
|
| 76 |
+
- عرض محتوى احتياطي في حالة عدم وجود المحتوى المتوقع
|
| 77 |
+
|
| 78 |
+
## 🧪 أدوات التشخيص المُنشأة
|
| 79 |
+
|
| 80 |
+
### 1. `diagnose_summary.py`
|
| 81 |
+
نظام تشخيص شامل يفحص:
|
| 82 |
+
- حالة العمليات والمنافذ
|
| 83 |
+
- إعدادات CORS
|
| 84 |
+
- وظيفة التلخيص
|
| 85 |
+
- المكتبات المطلوبة
|
| 86 |
+
|
| 87 |
+
### 2. `test_summary_button.py`
|
| 88 |
+
اختبار مبسط ومباشر لزر التلخيص
|
| 89 |
+
|
| 90 |
+
### 3. `test_summarize.py`
|
| 91 |
+
اختبار أساسي لـ endpoint التلخيص
|
| 92 |
+
|
| 93 |
+
## 📊 نتائج الاختبار النهائية ✅
|
| 94 |
+
|
| 95 |
+
```
|
| 96 |
+
🎉 جميع الاختبارات نجحت!
|
| 97 |
+
✅ زر التلخيص يعمل بشكل صحيح
|
| 98 |
+
|
| 99 |
+
📊 ملخص التشخيص:
|
| 100 |
+
📦 المكتبات: ✅ موجودة
|
| 101 |
+
🔧 عملية Python: ✅ تعمل
|
| 102 |
+
🌐 المنفذ 5001: ✅ مفتوح
|
| 103 |
+
🔧 CORS: ✅ صحيح
|
| 104 |
+
🤖 التلخيص: ✅ يعمل
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
## 🔧 الملفات المُعدّلة
|
| 108 |
+
|
| 109 |
+
### 1. `recorder_server.py`
|
| 110 |
+
- إصلاح إعدادات CORS
|
| 111 |
+
- إزالة التكرار في headers
|
| 112 |
+
- تبسيط معالجة OPTIONS requests
|
| 113 |
+
|
| 114 |
+
### 2. `templates/recorder.html`
|
| 115 |
+
- تحسين دالة `generateSummary()`
|
| 116 |
+
- تحسين دالة `displaySummaryResults()`
|
| 117 |
+
- رسائل خطأ أوضح
|
| 118 |
+
|
| 119 |
+
### 3. ملفات التشخيص الجديدة
|
| 120 |
+
- `diagnose_summary.py`
|
| 121 |
+
- `test_summary_button.py`
|
| 122 |
+
- `test_summarize.py`
|
| 123 |
+
|
| 124 |
+
## 🚀 كيفية التحقق من الحل
|
| 125 |
+
|
| 126 |
+
### 1. تشغيل الخادم:
|
| 127 |
+
```bash
|
| 128 |
+
python recorder_server.py
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
### 2. تشغيل التشخيص:
|
| 132 |
+
```bash
|
| 133 |
+
python diagnose_summary.py
|
| 134 |
+
```
|
| 135 |
+
|
| 136 |
+
### 3. اختبار زر التلخيص:
|
| 137 |
+
```bash
|
| 138 |
+
python test_summary_button.py
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
### 4. اختبار من الواجهة:
|
| 142 |
+
1. افتح `http://localhost:5054`
|
| 143 |
+
2. سجل صوت أو ادخل نص
|
| 144 |
+
3. اضغط زر "🤖 Generate Smart Lecture Summary"
|
| 145 |
+
4. تأكد من ظهور الملخص
|
| 146 |
+
|
| 147 |
+
## 💡 نصائح للمستقبل
|
| 148 |
+
|
| 149 |
+
### 1. تجنب تكرار CORS
|
| 150 |
+
- استخدم إعداد CORS واحد فقط
|
| 151 |
+
- لا تضع إعدادات يدوية إضافية
|
| 152 |
+
|
| 153 |
+
### 2. مراقبة التبعيات
|
| 154 |
+
- تأكد من تثبيت جميع المكتبات المطلوبة
|
| 155 |
+
- استخدم `requirements.txt` محدث
|
| 156 |
+
|
| 157 |
+
### 3. استخدام أدوات التشخيص
|
| 158 |
+
- شغل `diagnose_summary.py` عند مواجهة مشاكل
|
| 159 |
+
- يوفر تشخيص سريع وشامل
|
| 160 |
+
|
| 161 |
+
## 🎯 الخلاصة
|
| 162 |
+
|
| 163 |
+
تم حل مشكلة زر التلخيص بنجاح من خلال:
|
| 164 |
+
1. ✅ إصلاح مشكلة CORS المزدوجة
|
| 165 |
+
2. ✅ تثبيت المكتبات المفقودة
|
| 166 |
+
3. ✅ تحسين معالجة الأخطاء
|
| 167 |
+
4. ✅ إنشاء أدوات تشخيص شاملة
|
| 168 |
+
|
| 169 |
+
**النتيجة: زر التلخيص يعمل بشكل مثالي الآن! 🎉**
|
TECHNICAL_IMPLEMENTATION.md
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SyncMaster Enhanced - Technical Implementation Summary
|
| 2 |
+
|
| 3 |
+
## 🎯 Summary of Enhancements
|
| 4 |
+
|
| 5 |
+
This document outlines the comprehensive improvements made to SyncMaster to support AI-powered translation for international students.
|
| 6 |
+
|
| 7 |
+
## 🔧 New Components Added
|
| 8 |
+
|
| 9 |
+
### 1. `translator.py` - AI Translation Engine
|
| 10 |
+
```python
|
| 11 |
+
class AITranslator:
|
| 12 |
+
- translate_text(text, target_language='ar', source_language='auto')
|
| 13 |
+
- detect_language(text)
|
| 14 |
+
- translate_ui_elements(ui_dict, target_language='ar')
|
| 15 |
+
- batch_translate(texts, target_language='ar')
|
| 16 |
+
```
|
| 17 |
+
|
| 18 |
+
**Features:**
|
| 19 |
+
- Gemini AI-powered translation
|
| 20 |
+
- Academic content optimization
|
| 21 |
+
- Multi-language support (Arabic, English, French, Spanish)
|
| 22 |
+
- Batch processing capabilities
|
| 23 |
+
- Context-aware translation
|
| 24 |
+
|
| 25 |
+
### 2. Enhanced `audio_processor.py`
|
| 26 |
+
```python
|
| 27 |
+
class AudioProcessor:
|
| 28 |
+
- get_word_timestamps_with_translation(audio_file_path, target_language='ar')
|
| 29 |
+
- batch_translate_transcription(audio_file_path, target_languages)
|
| 30 |
+
- _create_translated_timestamps(original_timestamps, original_text, translated_text)
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
**New Features:**
|
| 34 |
+
- Integrated translation with transcription
|
| 35 |
+
- Proportional timestamp mapping for translated text
|
| 36 |
+
- Multi-language processing
|
| 37 |
+
- Enhanced error handling and logging
|
| 38 |
+
|
| 39 |
+
### 3. Updated `recorder_server.py`
|
| 40 |
+
```python
|
| 41 |
+
@app.route('/record', methods=['POST'])
|
| 42 |
+
def record():
|
| 43 |
+
# Enhanced with translation parameters:
|
| 44 |
+
# - target_language
|
| 45 |
+
# - enable_translation
|
| 46 |
+
# - comprehensive response with both original and translated text
|
| 47 |
+
|
| 48 |
+
@app.route('/translate', methods=['POST'])
|
| 49 |
+
def translate_text():
|
| 50 |
+
# Standalone translation endpoint
|
| 51 |
+
|
| 52 |
+
@app.route('/languages', methods=['GET'])
|
| 53 |
+
def get_supported_languages():
|
| 54 |
+
# Get list of supported languages
|
| 55 |
+
|
| 56 |
+
@app.route('/ui-translations/<language>', methods=['GET'])
|
| 57 |
+
def get_ui_translations(language):
|
| 58 |
+
# Get UI translations for specific language
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
### 4. Enhanced `templates/recorder.html`
|
| 62 |
+
**New Features:**
|
| 63 |
+
- Multi-language interface (English/Arabic)
|
| 64 |
+
- RTL support for Arabic
|
| 65 |
+
- Translation toggle controls
|
| 66 |
+
- Target language selection
|
| 67 |
+
- Enhanced visual design
|
| 68 |
+
- Keyboard shortcuts
|
| 69 |
+
- Accessibility improvements
|
| 70 |
+
|
| 71 |
+
**UI Improvements:**
|
| 72 |
+
- Modern gradient design
|
| 73 |
+
- Responsive layout for mobile devices
|
| 74 |
+
- Real-time language switching
|
| 75 |
+
- Visual feedback for translation status
|
| 76 |
+
- Better error messaging
|
| 77 |
+
|
| 78 |
+
### 5. Updated `app.py` - Main Application
|
| 79 |
+
**Enhancements:**
|
| 80 |
+
- Language selection in sidebar
|
| 81 |
+
- Translation settings integration
|
| 82 |
+
- Enhanced processing workflow
|
| 83 |
+
- Bilingual interface support
|
| 84 |
+
- Improved user experience flow
|
| 85 |
+
|
| 86 |
+
## 🌐 Multi-Language Support Implementation
|
| 87 |
+
|
| 88 |
+
### UI Translation System
|
| 89 |
+
```python
|
| 90 |
+
UI_TRANSLATIONS = {
|
| 91 |
+
'en': { /* English translations */ },
|
| 92 |
+
'ar': { /* Arabic translations */ }
|
| 93 |
+
}
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
### Dynamic Language Switching
|
| 97 |
+
- Client-side language detection
|
| 98 |
+
- Server-side translation API
|
| 99 |
+
- Real-time UI updates
|
| 100 |
+
- RTL text direction support
|
| 101 |
+
|
| 102 |
+
### Translation Workflow
|
| 103 |
+
1. **Audio Recording** → Record with language preferences
|
| 104 |
+
2. **Transcription** → AI-powered speech-to-text
|
| 105 |
+
3. **Language Detection** → Automatic source language identification
|
| 106 |
+
4. **Translation** → Context-aware AI translation
|
| 107 |
+
5. **Presentation** → Side-by-side original and translated text
|
| 108 |
+
|
| 109 |
+
## 🚀 API Enhancements
|
| 110 |
+
|
| 111 |
+
### Recording Endpoint (`/record`)
|
| 112 |
+
**Request Parameters:**
|
| 113 |
+
```json
|
| 114 |
+
{
|
| 115 |
+
"audio_data": "binary_audio_file",
|
| 116 |
+
"markers": "[timestamp_array]",
|
| 117 |
+
"target_language": "ar|en|fr|es",
|
| 118 |
+
"enable_translation": "true|false"
|
| 119 |
+
}
|
| 120 |
+
```
|
| 121 |
+
|
| 122 |
+
**Response Format:**
|
| 123 |
+
```json
|
| 124 |
+
{
|
| 125 |
+
"success": true,
|
| 126 |
+
"original_text": "Original transcription",
|
| 127 |
+
"translated_text": "Translated text",
|
| 128 |
+
"file_path": "path/to/saved/file.json",
|
| 129 |
+
"markers": [timestamps],
|
| 130 |
+
"target_language": "ar",
|
| 131 |
+
"translation_enabled": true,
|
| 132 |
+
"translation_success": true,
|
| 133 |
+
"language_detected": "en"
|
| 134 |
+
}
|
| 135 |
+
```
|
| 136 |
+
|
| 137 |
+
### Translation Endpoint (`/translate`)
|
| 138 |
+
**Request:**
|
| 139 |
+
```json
|
| 140 |
+
{
|
| 141 |
+
"text": "Text to translate",
|
| 142 |
+
"target_language": "ar",
|
| 143 |
+
"source_language": "auto"
|
| 144 |
+
}
|
| 145 |
+
```
|
| 146 |
+
|
| 147 |
+
**Response:**
|
| 148 |
+
```json
|
| 149 |
+
{
|
| 150 |
+
"success": true,
|
| 151 |
+
"original_text": "Original text",
|
| 152 |
+
"translated_text": "النص المترجم",
|
| 153 |
+
"source_language": "en",
|
| 154 |
+
"target_language": "ar"
|
| 155 |
+
}
|
| 156 |
+
```
|
| 157 |
+
|
| 158 |
+
## 📱 Frontend Enhancements
|
| 159 |
+
|
| 160 |
+
### JavaScript Features
|
| 161 |
+
```javascript
|
| 162 |
+
// Language Management
|
| 163 |
+
async function loadTranslations(language)
|
| 164 |
+
function applyTranslations()
|
| 165 |
+
function changeLanguage()
|
| 166 |
+
|
| 167 |
+
// Enhanced Recording
|
| 168 |
+
function displayResults(result)
|
| 169 |
+
function displayMarkers(markers)
|
| 170 |
+
function showMessage(message, type)
|
| 171 |
+
|
| 172 |
+
// Keyboard Shortcuts
|
| 173 |
+
document.addEventListener('keydown', handleKeyboardShortcuts)
|
| 174 |
+
```
|
| 175 |
+
|
| 176 |
+
### CSS Improvements
|
| 177 |
+
```css
|
| 178 |
+
/* RTL Support */
|
| 179 |
+
html[dir="rtl"] { direction: rtl; }
|
| 180 |
+
|
| 181 |
+
/* Modern Design */
|
| 182 |
+
:root {
|
| 183 |
+
--primary-color: #4A90E2;
|
| 184 |
+
--success-color: #50C878;
|
| 185 |
+
/* ... more color variables */
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
/* Responsive Design */
|
| 189 |
+
@media (max-width: 768px) {
|
| 190 |
+
/* Mobile optimizations */
|
| 191 |
+
}
|
| 192 |
+
```
|
| 193 |
+
|
| 194 |
+
## 🔒 Security & Performance
|
| 195 |
+
|
| 196 |
+
### Security Measures
|
| 197 |
+
- Input validation for all API endpoints
|
| 198 |
+
- CORS configuration for cross-origin requests
|
| 199 |
+
- Secure file handling with temporary files
|
| 200 |
+
- API key protection in environment variables
|
| 201 |
+
|
| 202 |
+
### Performance Optimizations
|
| 203 |
+
- Parallel processing for audio and translation
|
| 204 |
+
- Efficient memory management
|
| 205 |
+
- Chunked audio processing
|
| 206 |
+
- Client-side caching for translations
|
| 207 |
+
|
| 208 |
+
## 📊 File Structure Changes
|
| 209 |
+
|
| 210 |
+
```
|
| 211 |
+
SyncMaster - Copy (2)/
|
| 212 |
+
├── translator.py # NEW: AI Translation engine
|
| 213 |
+
├── audio_processor.py # ENHANCED: With translation support
|
| 214 |
+
├── recorder_server.py # ENHANCED: Additional endpoints
|
| 215 |
+
├── app.py # ENHANCED: Multi-language support
|
| 216 |
+
├── templates/
|
| 217 |
+
│ └── recorder.html # ENHANCED: Multi-language UI
|
| 218 |
+
├── README_AR.md # NEW: Arabic documentation
|
| 219 |
+
├── setup_enhanced.py # NEW: Enhanced setup script
|
| 220 |
+
├── start_enhanced.bat # NEW: Quick start script
|
| 221 |
+
├── requirements.txt # UPDATED: Additional dependencies
|
| 222 |
+
└── .env # UPDATED: Additional configuration
|
| 223 |
+
```
|
| 224 |
+
|
| 225 |
+
## 🎓 Educational Features
|
| 226 |
+
|
| 227 |
+
### For International Students
|
| 228 |
+
1. **Language Barrier Reduction**: Real-time translation of lectures
|
| 229 |
+
2. **Better Comprehension**: Side-by-side original and translated text
|
| 230 |
+
3. **Cultural Adaptation**: Interface in native language
|
| 231 |
+
4. **Academic Context**: Specialized translation for educational content
|
| 232 |
+
|
| 233 |
+
### For Arabic Students
|
| 234 |
+
1. **Native Interface**: Complete Arabic UI
|
| 235 |
+
2. **Technical Term Translation**: English technical terms with Arabic explanations
|
| 236 |
+
3. **Reading Direction**: Proper RTL text display
|
| 237 |
+
4. **Cultural Context**: Academic content adapted for Arabic speakers
|
| 238 |
+
|
| 239 |
+
## 🔧 Installation & Setup
|
| 240 |
+
|
| 241 |
+
### Enhanced Setup Process
|
| 242 |
+
1. **Automated Installation**: `python setup_enhanced.py`
|
| 243 |
+
2. **Dependency Management**: Automatic package installation
|
| 244 |
+
3. **Configuration Validation**: Environment file checking
|
| 245 |
+
4. **Service Management**: Automatic server startup
|
| 246 |
+
|
| 247 |
+
### Quick Start Options
|
| 248 |
+
- **Windows**: `start_enhanced.bat`
|
| 249 |
+
- **Cross-platform**: `python setup_enhanced.py`
|
| 250 |
+
- **Manual**: Individual component startup
|
| 251 |
+
|
| 252 |
+
## 📈 Testing & Quality Assurance
|
| 253 |
+
|
| 254 |
+
### Translation Quality
|
| 255 |
+
- Academic content optimization
|
| 256 |
+
- Technical term preservation
|
| 257 |
+
- Context-aware translation
|
| 258 |
+
- Fallback mechanisms
|
| 259 |
+
|
| 260 |
+
### User Experience Testing
|
| 261 |
+
- Multi-language interface testing
|
| 262 |
+
- Mobile responsiveness
|
| 263 |
+
- Accessibility compliance
|
| 264 |
+
- Performance optimization
|
| 265 |
+
|
| 266 |
+
## 🔮 Future Enhancements
|
| 267 |
+
|
| 268 |
+
### Planned Features
|
| 269 |
+
1. **Advanced Translation**: Subject-specific terminology
|
| 270 |
+
2. **Collaboration Tools**: Shared study sessions
|
| 271 |
+
3. **Learning Analytics**: Progress tracking
|
| 272 |
+
4. **Platform Integration**: LMS connectivity
|
| 273 |
+
5. **Offline Support**: Local processing capabilities
|
| 274 |
+
|
| 275 |
+
### Technical Roadmap
|
| 276 |
+
1. **Model Optimization**: Faster processing
|
| 277 |
+
2. **Caching System**: Reduced API calls
|
| 278 |
+
3. **Advanced UI**: More interactive features
|
| 279 |
+
4. **Mobile App**: Native mobile application
|
| 280 |
+
|
| 281 |
+
---
|
| 282 |
+
|
| 283 |
+
## 📞 Technical Support
|
| 284 |
+
|
| 285 |
+
### Debugging Features
|
| 286 |
+
- Comprehensive logging system
|
| 287 |
+
- Browser console integration
|
| 288 |
+
- Error message localization
|
| 289 |
+
- Performance monitoring
|
| 290 |
+
|
| 291 |
+
### Troubleshooting Resources
|
| 292 |
+
- Detailed error messages
|
| 293 |
+
- Multi-language support documentation
|
| 294 |
+
- Community forum integration
|
| 295 |
+
- Technical FAQ
|
| 296 |
+
|
| 297 |
+
---
|
| 298 |
+
|
| 299 |
+
**This enhanced version of SyncMaster represents a significant advancement in making educational technology accessible to international students worldwide.**
|
TROUBLESHOOTING.md
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🛠️ دليل استكشاف الأخطاء - SyncMaster Enhanced
|
| 2 |
+
# Troubleshooting Guide - SyncMaster Enhanced
|
| 3 |
+
|
| 4 |
+
## 🔍 الأخطاء الشائعة وحلولها / Common Errors and Solutions
|
| 5 |
+
|
| 6 |
+
### 1. خطأ الاتصال بالخادم / Server Connection Error
|
| 7 |
+
```
|
| 8 |
+
Error: Failed to fetch
|
| 9 |
+
POST http://localhost:5001/record net::ERR_CONNECTION_REFUSED
|
| 10 |
+
```
|
| 11 |
+
|
| 12 |
+
**الأسباب المحتملة / Possible Causes:**
|
| 13 |
+
- الخادم غير يعمل / Server not running
|
| 14 |
+
- منفذ 5001 مستخدم من برنامج آخر / Port 5001 used by another application
|
| 15 |
+
- جدار حماية يحجب الاتصال / Firewall blocking connection
|
| 16 |
+
|
| 17 |
+
**الحلول / Solutions:**
|
| 18 |
+
|
| 19 |
+
#### أ) تشغيل اختبار النظام / Run System Test:
|
| 20 |
+
```bash
|
| 21 |
+
python test_system.py
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
#### ب) تشغيل الخادم يدوياً / Start Server Manually:
|
| 25 |
+
```bash
|
| 26 |
+
# إيقاف جميع العمليات / Stop all processes
|
| 27 |
+
taskkill /f /im python.exe
|
| 28 |
+
|
| 29 |
+
# تشغيل الخادم / Start server
|
| 30 |
+
python recorder_server.py
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
#### ج) استخدام البدء المتقدم / Use Debug Startup:
|
| 34 |
+
```bash
|
| 35 |
+
python start_debug.py
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
#### د) فحص المنافذ / Check Ports:
|
| 39 |
+
```bash
|
| 40 |
+
# Windows
|
| 41 |
+
netstat -an | findstr :5001
|
| 42 |
+
|
| 43 |
+
# Linux/Mac
|
| 44 |
+
lsof -i :5001
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
### 2. مشكلة مفتاح API / API Key Issues
|
| 48 |
+
```
|
| 49 |
+
ERROR: GEMINI_API_KEY not found in environment variables
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
**الحل / Solution:**
|
| 53 |
+
1. تأكد من وجود ملف `.env`:
|
| 54 |
+
```bash
|
| 55 |
+
# إنشاء ملف .env / Create .env file
|
| 56 |
+
echo GEMINI_API_KEY=your_actual_api_key_here > .env
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
2. احصل على مفتاح API من:
|
| 60 |
+
- [Google AI Studio](https://makersuite.google.com/app/apikey)
|
| 61 |
+
|
| 62 |
+
3. أضف المفتاح إلى `.env`:
|
| 63 |
+
```
|
| 64 |
+
GEMINI_API_KEY=AIzaSyAS7JtrXjlNjyuo3RG5z6rkwocCwFy1YuA
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
### 3. مشاكل الصوت / Audio Issues
|
| 68 |
+
```
|
| 69 |
+
UserWarning: PySoundFile failed. Trying audioread instead.
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
**الحلول / Solutions:**
|
| 73 |
+
|
| 74 |
+
#### أ) تثبيت SoundFile مرة أخرى / Reinstall SoundFile:
|
| 75 |
+
```bash
|
| 76 |
+
pip uninstall soundfile
|
| 77 |
+
pip install soundfile
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
#### ب) تثبيت FFmpeg (إذا لزم الأمر) / Install FFmpeg if needed:
|
| 81 |
+
```bash
|
| 82 |
+
# Windows (using chocolatey)
|
| 83 |
+
choco install ffmpeg
|
| 84 |
+
|
| 85 |
+
# Or download from: https://ffmpeg.org/download.html
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
#### ج) فحص تنسيق الملف / Check Audio Format:
|
| 89 |
+
- استخدم WAV بدلاً من MP3
|
| 90 |
+
- تأكد من جودة التسجيل
|
| 91 |
+
|
| 92 |
+
### 4. مشاكل الترجمة / Translation Issues
|
| 93 |
+
```
|
| 94 |
+
WARNING: Gemini returned empty translation response
|
| 95 |
+
```
|
| 96 |
+
|
| 97 |
+
**الحلول / Solutions:**
|
| 98 |
+
|
| 99 |
+
#### أ) فحص اتصال الإنترنت / Check Internet Connection:
|
| 100 |
+
```bash
|
| 101 |
+
ping google.com
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
#### ب) اختبار مفتاح API / Test API Key:
|
| 105 |
+
```python
|
| 106 |
+
python test_system.py
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
#### ج) تغيير النموذج / Change Model:
|
| 110 |
+
- إذا فشل `gemini-2.5-flash`، جرب `gemini-1.5-flash`
|
| 111 |
+
|
| 112 |
+
### 5. مشاكل الواجهة / UI Issues
|
| 113 |
+
|
| 114 |
+
#### أ) الواجهة لا تحمّل / Interface Won't Load:
|
| 115 |
+
```bash
|
| 116 |
+
# تحقق من المنفذ / Check port
|
| 117 |
+
python -c "import socket; s=socket.socket(); s.bind(('',8501)); print('Port 8501 available')"
|
| 118 |
+
|
| 119 |
+
# تشغيل على منفذ مختلف / Run on different port
|
| 120 |
+
streamlit run app.py --server.port 8502
|
| 121 |
+
```
|
| 122 |
+
|
| 123 |
+
#### ب) مشاكل اللغة العربية / Arabic Language Issues:
|
| 124 |
+
- تأكد من دعم المتصفح للـ RTL
|
| 125 |
+
- استخدم Chrome أو Firefox للأفضل
|
| 126 |
+
|
| 127 |
+
### 6. مشاكل الأداء / Performance Issues
|
| 128 |
+
|
| 129 |
+
#### أ) بطء في المعالجة / Slow Processing:
|
| 130 |
+
- تحقق من سرعة الإنترنت
|
| 131 |
+
- قلل حجم الملف الصوتي
|
| 132 |
+
- استخدم جودة أقل للتسجيل
|
| 133 |
+
|
| 134 |
+
#### ب) استهلاك ذاكرة عالي / High Memory Usage:
|
| 135 |
+
```bash
|
| 136 |
+
# إعادة تشغيل النظام / Restart system
|
| 137 |
+
python start_debug.py
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
## 🔧 أدوات التشخيص / Diagnostic Tools
|
| 141 |
+
|
| 142 |
+
### 1. اختبار شامل / Complete Test:
|
| 143 |
+
```bash
|
| 144 |
+
python test_system.py
|
| 145 |
+
```
|
| 146 |
+
|
| 147 |
+
### 2. فحص المنافذ / Port Check:
|
| 148 |
+
```python
|
| 149 |
+
python -c "
|
| 150 |
+
import socket
|
| 151 |
+
ports = [5001, 8501, 8502]
|
| 152 |
+
for port in ports:
|
| 153 |
+
try:
|
| 154 |
+
s = socket.socket()
|
| 155 |
+
s.bind(('localhost', port))
|
| 156 |
+
s.close()
|
| 157 |
+
print(f'Port {port}: Available ✅')
|
| 158 |
+
except:
|
| 159 |
+
print(f'Port {port}: Busy ❌')
|
| 160 |
+
"
|
| 161 |
+
```
|
| 162 |
+
|
| 163 |
+
### 3. فحص التبعيات / Dependencies Check:
|
| 164 |
+
```bash
|
| 165 |
+
pip list | grep -E "(streamlit|flask|librosa|soundfile|google-generativeai)"
|
| 166 |
+
```
|
| 167 |
+
|
| 168 |
+
### 4. فحص العمليات / Process Check:
|
| 169 |
+
```bash
|
| 170 |
+
# Windows
|
| 171 |
+
tasklist | findstr python
|
| 172 |
+
|
| 173 |
+
# Linux/Mac
|
| 174 |
+
ps aux | grep python
|
| 175 |
+
```
|
| 176 |
+
|
| 177 |
+
## 📱 نصائح لحل المشاكل / Troubleshooting Tips
|
| 178 |
+
|
| 179 |
+
### للطلاب الجدد / For New Users:
|
| 180 |
+
1. **ابدأ بالاختبار الشامل / Start with system test**:
|
| 181 |
+
```bash
|
| 182 |
+
python test_system.py
|
| 183 |
+
```
|
| 184 |
+
|
| 185 |
+
2. **استخدم البدء المتقدم / Use debug startup**:
|
| 186 |
+
```bash
|
| 187 |
+
python start_debug.py
|
| 188 |
+
```
|
| 189 |
+
|
| 190 |
+
3. **تحقق من المتطلبات / Check requirements**:
|
| 191 |
+
- Python 3.8+
|
| 192 |
+
- مفتاح Gemini API صالح
|
| 193 |
+
- اتصال إنترنت مستقر
|
| 194 |
+
|
| 195 |
+
### للطلاب المتقدمين / For Advanced Users:
|
| 196 |
+
1. **مراجعة السجلات / Check logs**:
|
| 197 |
+
- افتح console المتصفح (F12)
|
| 198 |
+
- راجع سجلات الطرفية
|
| 199 |
+
|
| 200 |
+
2. **تخصيص الإعدادات / Customize settings**:
|
| 201 |
+
- غير المنافذ في حالة التضارب
|
| 202 |
+
- عدّل إعدادات الصوت
|
| 203 |
+
|
| 204 |
+
3. **التشخيص المتقدم / Advanced diagnostics**:
|
| 205 |
+
```python
|
| 206 |
+
# اختبار الاتصال / Test connection
|
| 207 |
+
import requests
|
| 208 |
+
response = requests.get('http://localhost:5001/record')
|
| 209 |
+
print(response.status_code, response.text)
|
| 210 |
+
```
|
| 211 |
+
|
| 212 |
+
## 🆘 طلب المساعدة / Getting Help
|
| 213 |
+
|
| 214 |
+
### معلومات مطلوبة / Required Information:
|
| 215 |
+
1. نظام التشغيل / Operating System
|
| 216 |
+
2. إصدار Python / Python Version
|
| 217 |
+
3. نتائج `python test_system.py`
|
| 218 |
+
4. رسائل الخطأ الكاملة / Complete error messages
|
| 219 |
+
5. سجلات الطرفية / Terminal logs
|
| 220 |
+
|
| 221 |
+
### خطوات الإبلاغ / Reporting Steps:
|
| 222 |
+
1. شغّل الاختبار الشامل
|
| 223 |
+
2. احفظ النتائج
|
| 224 |
+
3. صوّر رسائل الخطأ
|
| 225 |
+
4. اذكر الخطوات التي أدت للمشكلة
|
| 226 |
+
|
| 227 |
+
---
|
| 228 |
+
|
| 229 |
+
## 🎯 Quick Fix Commands / أوامر الإصلاح السريع
|
| 230 |
+
|
| 231 |
+
```bash
|
| 232 |
+
# إعادة تعيين كامل / Complete Reset
|
| 233 |
+
taskkill /f /im python.exe
|
| 234 |
+
python test_system.py
|
| 235 |
+
python start_debug.py
|
| 236 |
+
|
| 237 |
+
# إصلاح التبعيات / Fix Dependencies
|
| 238 |
+
pip install --upgrade -r requirements.txt
|
| 239 |
+
|
| 240 |
+
# إصلاح المنافذ / Fix Ports
|
| 241 |
+
python start_debug.py
|
| 242 |
+
|
| 243 |
+
# اختبار الترجمة / Test Translation
|
| 244 |
+
python -c "from translator import AITranslator; t=AITranslator(); print(t.translate_text('Hello', 'ar'))"
|
| 245 |
+
```
|
| 246 |
+
|
| 247 |
+
---
|
| 248 |
+
|
| 249 |
+
**تذكر: معظم المشاكل تُحل بإعادة تشغيل النظام وتشغيل الاختبار الشامل! 🔄**
|
| 250 |
+
|
| 251 |
+
**Remember: Most issues are solved by restarting and running the system test! 🔄**
|
ai_questions.py
ADDED
|
@@ -0,0 +1,773 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ai_questions.py - AI-Powered Question Engine for SyncMaster
|
| 2 |
+
|
| 3 |
+
import time
|
| 4 |
+
import hashlib
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
from typing import Dict, List, Optional, Tuple, Any
|
| 7 |
+
from dataclasses import dataclass, field
|
| 8 |
+
import streamlit as st
|
| 9 |
+
|
| 10 |
+
@dataclass
|
| 11 |
+
class QAPair:
|
| 12 |
+
"""Represents a question-answer pair"""
|
| 13 |
+
question: str
|
| 14 |
+
answer: str
|
| 15 |
+
timestamp: datetime
|
| 16 |
+
question_type: str # 'template' or 'custom'
|
| 17 |
+
response_time_ms: int
|
| 18 |
+
model_used: str = "Unknown" # Which AI model was used
|
| 19 |
+
|
| 20 |
+
@dataclass
|
| 21 |
+
class QuestionSession:
|
| 22 |
+
"""Represents a question session for a specific text segment"""
|
| 23 |
+
session_id: str
|
| 24 |
+
selected_text: str
|
| 25 |
+
segment_id: str
|
| 26 |
+
start_timestamp: int
|
| 27 |
+
ui_language: str
|
| 28 |
+
conversation: List[QAPair] = field(default_factory=list)
|
| 29 |
+
created_at: datetime = field(default_factory=datetime.now)
|
| 30 |
+
|
| 31 |
+
@dataclass
|
| 32 |
+
class TextSelection:
|
| 33 |
+
"""Represents selected text from broadcast"""
|
| 34 |
+
text: str
|
| 35 |
+
segment_id: str
|
| 36 |
+
start_ms: int
|
| 37 |
+
end_ms: int
|
| 38 |
+
translations: Dict[str, str] = field(default_factory=dict)
|
| 39 |
+
selection_timestamp: int = field(default_factory=lambda: int(time.time() * 1000))
|
| 40 |
+
|
| 41 |
+
class AIQuestionEngine:
|
| 42 |
+
"""
|
| 43 |
+
AI-powered question engine for interactive learning
|
| 44 |
+
"""
|
| 45 |
+
|
| 46 |
+
def __init__(self, translator_instance=None):
|
| 47 |
+
self.translator = translator_instance
|
| 48 |
+
self.conversation_history: Dict[str, QuestionSession] = {}
|
| 49 |
+
self.model_usage_stats = {
|
| 50 |
+
'Gemini AI': 0,
|
| 51 |
+
'Groq AI': 0,
|
| 52 |
+
'OpenRouter AI': 0,
|
| 53 |
+
'Simple Response': 0
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
# Question templates in multiple languages
|
| 57 |
+
self.question_templates = {
|
| 58 |
+
'ar': [
|
| 59 |
+
"اشرح هذا النص بالتفصيل",
|
| 60 |
+
"أعطني أمثلة عملية على هذا",
|
| 61 |
+
"ما معنى هذا المصطلح؟",
|
| 62 |
+
"كيف يُستخدم هذا في الواقع؟",
|
| 63 |
+
"ما أهمية هذا الموضوع؟",
|
| 64 |
+
"ما هي النقاط الرئيسية هنا؟",
|
| 65 |
+
"اربط هذا بمفاهيم أخرى",
|
| 66 |
+
"ما هي التطبيقات العملية؟"
|
| 67 |
+
],
|
| 68 |
+
'en': [
|
| 69 |
+
"Explain this text in detail",
|
| 70 |
+
"Give me practical examples of this",
|
| 71 |
+
"What does this term mean?",
|
| 72 |
+
"How is this used in practice?",
|
| 73 |
+
"Why is this topic important?",
|
| 74 |
+
"What are the key points here?",
|
| 75 |
+
"Connect this to other concepts",
|
| 76 |
+
"What are the practical applications?"
|
| 77 |
+
],
|
| 78 |
+
'fr': [
|
| 79 |
+
"Expliquez ce texte en détail",
|
| 80 |
+
"Donnez-moi des exemples pratiques",
|
| 81 |
+
"Que signifie ce terme?",
|
| 82 |
+
"Comment cela est-il utilisé en pratique?",
|
| 83 |
+
"Pourquoi ce sujet est-il important?",
|
| 84 |
+
"Quels sont les points clés ici?",
|
| 85 |
+
"Reliez cela à d'autres concepts",
|
| 86 |
+
"Quelles sont les applications pratiques?"
|
| 87 |
+
],
|
| 88 |
+
'es': [
|
| 89 |
+
"Explica este texto en detalle",
|
| 90 |
+
"Dame ejemplos prácticos de esto",
|
| 91 |
+
"¿Qué significa este término?",
|
| 92 |
+
"¿Cómo se usa esto en la práctica?",
|
| 93 |
+
"¿Por qué es importante este tema?",
|
| 94 |
+
"¿Cuáles son los puntos clave aquí?",
|
| 95 |
+
"Conecta esto con otros conceptos",
|
| 96 |
+
"¿Cuáles son las aplicaciones prácticas?"
|
| 97 |
+
],
|
| 98 |
+
'de': [
|
| 99 |
+
"Erkläre diesen Text im Detail",
|
| 100 |
+
"Gib mir praktische Beispiele dafür",
|
| 101 |
+
"Was bedeutet dieser Begriff?",
|
| 102 |
+
"Wie wird das in der Praxis verwendet?",
|
| 103 |
+
"Warum ist dieses Thema wichtig?",
|
| 104 |
+
"Was sind die wichtigsten Punkte hier?",
|
| 105 |
+
"Verbinde das mit anderen Konzepten",
|
| 106 |
+
"Was sind die praktischen Anwendungen?"
|
| 107 |
+
],
|
| 108 |
+
'zh': [
|
| 109 |
+
"详细解释这段文字",
|
| 110 |
+
"给我一些实际例子",
|
| 111 |
+
"这个术语是什么意思?",
|
| 112 |
+
"这在实践中如何使用?",
|
| 113 |
+
"为什么这个话题很重要?",
|
| 114 |
+
"这里的要点是什么?",
|
| 115 |
+
"将此与其他概念联系起来",
|
| 116 |
+
"实际应用有哪些?"
|
| 117 |
+
]
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
# Response formatting templates
|
| 121 |
+
self.response_templates = {
|
| 122 |
+
'ar': {
|
| 123 |
+
'context_intro': "بناءً على النص المحدد:",
|
| 124 |
+
'explanation_intro': "الشرح:",
|
| 125 |
+
'examples_intro': "أمثلة:",
|
| 126 |
+
'importance_intro': "الأهمية:",
|
| 127 |
+
'applications_intro': "التطبيقات:",
|
| 128 |
+
'error_message': "عذراً، حدث خطأ في معالجة سؤالك. يرجى المحاولة مرة أخرى."
|
| 129 |
+
},
|
| 130 |
+
'en': {
|
| 131 |
+
'context_intro': "Based on the selected text:",
|
| 132 |
+
'explanation_intro': "Explanation:",
|
| 133 |
+
'examples_intro': "Examples:",
|
| 134 |
+
'importance_intro': "Importance:",
|
| 135 |
+
'applications_intro': "Applications:",
|
| 136 |
+
'error_message': "Sorry, there was an error processing your question. Please try again."
|
| 137 |
+
}
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
def get_question_templates(self, ui_language: str = 'ar') -> List[str]:
|
| 141 |
+
"""Get pre-defined question templates for the specified language"""
|
| 142 |
+
return self.question_templates.get(ui_language, self.question_templates['ar'])
|
| 143 |
+
|
| 144 |
+
def create_session_id(self, selected_text: str, segment_id: str) -> str:
|
| 145 |
+
"""Create a unique session ID for a text selection"""
|
| 146 |
+
content = f"{selected_text}_{segment_id}_{int(time.time())}"
|
| 147 |
+
return hashlib.md5(content.encode()).hexdigest()[:12]
|
| 148 |
+
|
| 149 |
+
def process_question(self,
|
| 150 |
+
selected_text: str,
|
| 151 |
+
question: str,
|
| 152 |
+
segment_info: Dict[str, Any],
|
| 153 |
+
ui_language: str = 'ar',
|
| 154 |
+
session_id: Optional[str] = None,
|
| 155 |
+
preferred_model: str = 'auto') -> Tuple[Optional[str], Optional[str], str]:
|
| 156 |
+
"""
|
| 157 |
+
Process a user question about selected text
|
| 158 |
+
|
| 159 |
+
Args:
|
| 160 |
+
selected_text: The text the user selected
|
| 161 |
+
question: The user's question
|
| 162 |
+
segment_info: Information about the broadcast segment
|
| 163 |
+
ui_language: UI language ('ar' or 'en')
|
| 164 |
+
session_id: Existing session ID or None for new session
|
| 165 |
+
|
| 166 |
+
Returns:
|
| 167 |
+
Tuple of (answer, error_message, session_id)
|
| 168 |
+
"""
|
| 169 |
+
|
| 170 |
+
if not self.translator:
|
| 171 |
+
error_msg = self.response_templates[ui_language]['error_message']
|
| 172 |
+
return None, error_msg, session_id or ""
|
| 173 |
+
|
| 174 |
+
try:
|
| 175 |
+
# Create or get session
|
| 176 |
+
if not session_id:
|
| 177 |
+
session_id = self.create_session_id(selected_text, segment_info.get('id', ''))
|
| 178 |
+
session = QuestionSession(
|
| 179 |
+
session_id=session_id,
|
| 180 |
+
selected_text=selected_text,
|
| 181 |
+
segment_id=segment_info.get('id', ''),
|
| 182 |
+
start_timestamp=segment_info.get('start_ms', 0),
|
| 183 |
+
ui_language=ui_language
|
| 184 |
+
)
|
| 185 |
+
self.conversation_history[session_id] = session
|
| 186 |
+
else:
|
| 187 |
+
session = self.conversation_history.get(session_id)
|
| 188 |
+
if not session:
|
| 189 |
+
# Session not found, create new one
|
| 190 |
+
session = QuestionSession(
|
| 191 |
+
session_id=session_id,
|
| 192 |
+
selected_text=selected_text,
|
| 193 |
+
segment_id=segment_info.get('id', ''),
|
| 194 |
+
start_timestamp=segment_info.get('start_ms', 0),
|
| 195 |
+
ui_language=ui_language
|
| 196 |
+
)
|
| 197 |
+
self.conversation_history[session_id] = session
|
| 198 |
+
|
| 199 |
+
# Prepare context for AI
|
| 200 |
+
context = self._prepare_question_context(selected_text, question, session, ui_language)
|
| 201 |
+
|
| 202 |
+
# Get AI response with preferred model
|
| 203 |
+
start_time = time.time()
|
| 204 |
+
ai_response, error, model_used = self.get_ai_response_with_model(context, ui_language, preferred_model)
|
| 205 |
+
response_time = int((time.time() - start_time) * 1000)
|
| 206 |
+
|
| 207 |
+
if ai_response:
|
| 208 |
+
# Format response
|
| 209 |
+
formatted_response = self.format_ai_response(ai_response, ui_language)
|
| 210 |
+
|
| 211 |
+
# Save to conversation history with model info
|
| 212 |
+
question_type = 'template' if question in self.get_question_templates(ui_language) else 'custom'
|
| 213 |
+
qa_pair = QAPair(
|
| 214 |
+
question=question,
|
| 215 |
+
answer=formatted_response,
|
| 216 |
+
timestamp=datetime.now(),
|
| 217 |
+
question_type=question_type,
|
| 218 |
+
response_time_ms=response_time
|
| 219 |
+
)
|
| 220 |
+
# Add model info to the QA pair
|
| 221 |
+
qa_pair.model_used = model_used
|
| 222 |
+
session.conversation.append(qa_pair)
|
| 223 |
+
|
| 224 |
+
return formatted_response, None, session_id, model_used
|
| 225 |
+
else:
|
| 226 |
+
error_msg = error or self.response_templates[ui_language]['error_message']
|
| 227 |
+
return None, error_msg, session_id, None
|
| 228 |
+
|
| 229 |
+
except Exception as e:
|
| 230 |
+
error_msg = f"{self.response_templates[ui_language]['error_message']} ({str(e)})"
|
| 231 |
+
return None, error_msg, session_id or ""
|
| 232 |
+
|
| 233 |
+
def _prepare_question_context(self,
|
| 234 |
+
selected_text: str,
|
| 235 |
+
question: str,
|
| 236 |
+
session: QuestionSession,
|
| 237 |
+
ui_language: str) -> str:
|
| 238 |
+
"""Prepare context for AI question processing"""
|
| 239 |
+
|
| 240 |
+
templates = self.response_templates[ui_language]
|
| 241 |
+
|
| 242 |
+
# Build context with conversation history
|
| 243 |
+
context_parts = []
|
| 244 |
+
|
| 245 |
+
# Add selected text context
|
| 246 |
+
context_parts.append(f"{templates['context_intro']}")
|
| 247 |
+
context_parts.append(f'"{selected_text}"')
|
| 248 |
+
context_parts.append("")
|
| 249 |
+
|
| 250 |
+
# Add conversation history if exists
|
| 251 |
+
if session.conversation:
|
| 252 |
+
context_parts.append("Previous conversation:")
|
| 253 |
+
for qa in session.conversation[-3:]: # Last 3 Q&A pairs for context
|
| 254 |
+
context_parts.append(f"Q: {qa.question}")
|
| 255 |
+
context_parts.append(f"A: {qa.answer[:200]}...") # Truncate long answers
|
| 256 |
+
context_parts.append("")
|
| 257 |
+
|
| 258 |
+
# Add current question
|
| 259 |
+
context_parts.append(f"Current question: {question}")
|
| 260 |
+
context_parts.append("")
|
| 261 |
+
|
| 262 |
+
# Add instructions based on language
|
| 263 |
+
language_instructions = {
|
| 264 |
+
'ar': """
|
| 265 |
+
أجب على السؤال بناءً على النص المحدد. اجعل إجابتك:
|
| 266 |
+
- واضحة ومفهومة
|
| 267 |
+
- مرتبطة بالنص المحدد
|
| 268 |
+
- تحتوي على أمثلة عملية إذا كان ذلك مناسباً
|
| 269 |
+
- باللغة العربية الفصحى
|
| 270 |
+
- منظمة ومنسقة بشكل جيد
|
| 271 |
+
|
| 272 |
+
إذا كان السؤال يطلب شرحاً، قدم شرحاً مفصلاً.
|
| 273 |
+
إذا كان يطلب أمثلة، قدم أمثلة واقعية ومفيدة.
|
| 274 |
+
إذا كان يطلب التوضيح، اشرح المفاهيم بطريقة بسيطة.
|
| 275 |
+
""",
|
| 276 |
+
'en': """
|
| 277 |
+
Answer the question based on the selected text. Make your answer:
|
| 278 |
+
- Clear and understandable
|
| 279 |
+
- Related to the selected text
|
| 280 |
+
- Include practical examples when appropriate
|
| 281 |
+
- In English
|
| 282 |
+
- Well-organized and formatted
|
| 283 |
+
|
| 284 |
+
If the question asks for explanation, provide detailed explanation.
|
| 285 |
+
If it asks for examples, provide real-world, helpful examples.
|
| 286 |
+
If it asks for clarification, explain concepts in simple terms.
|
| 287 |
+
""",
|
| 288 |
+
'fr': """
|
| 289 |
+
Répondez à la question basée sur le texte sélectionné. Rendez votre réponse:
|
| 290 |
+
- Claire et compréhensible
|
| 291 |
+
- Liée au texte sélectionné
|
| 292 |
+
- Incluez des exemples pratiques si approprié
|
| 293 |
+
- En français
|
| 294 |
+
- Bien organisée et formatée
|
| 295 |
+
|
| 296 |
+
Si la question demande une explication, fournissez une explication détaillée.
|
| 297 |
+
Si elle demande des exemples, fournissez des exemples réels et utiles.
|
| 298 |
+
Si elle demande des clarifications, expliquez les concepts en termes simples.
|
| 299 |
+
""",
|
| 300 |
+
'es': """
|
| 301 |
+
Responde la pregunta basada en el texto seleccionado. Haz que tu respuesta sea:
|
| 302 |
+
- Clara y comprensible
|
| 303 |
+
- Relacionada con el texto seleccionado
|
| 304 |
+
- Incluye ejemplos prácticos cuando sea apropiado
|
| 305 |
+
- En español
|
| 306 |
+
- Bien organizada y formateada
|
| 307 |
+
|
| 308 |
+
Si la pregunta pide explicación, proporciona explicación detallada.
|
| 309 |
+
Si pide ejemplos, proporciona ejemplos reales y útiles.
|
| 310 |
+
Si pide aclaración, explica conceptos en términos simples.
|
| 311 |
+
""",
|
| 312 |
+
'de': """
|
| 313 |
+
Beantworte die Frage basierend auf dem ausgewählten Text. Mache deine Antwort:
|
| 314 |
+
- Klar und verständlich
|
| 315 |
+
- Bezogen auf den ausgewählten Text
|
| 316 |
+
- Enthalte praktische Beispiele wenn angemessen
|
| 317 |
+
- Auf Deutsch
|
| 318 |
+
- Gut organisiert und formatiert
|
| 319 |
+
|
| 320 |
+
Wenn die Frage nach Erklärung fragt, gib detaillierte Erklärung.
|
| 321 |
+
Wenn sie nach Beispielen fragt, gib reale und hilfreiche Beispiele.
|
| 322 |
+
Wenn sie nach Klarstellung fragt, erkläre Konzepte in einfachen Begriffen.
|
| 323 |
+
""",
|
| 324 |
+
'zh': """
|
| 325 |
+
根据选定的文本回答问题。让你的回答:
|
| 326 |
+
- 清晰易懂
|
| 327 |
+
- 与选定文本相关
|
| 328 |
+
- 适当时包含实际例子
|
| 329 |
+
- 用中文
|
| 330 |
+
- 组织良好且格式化
|
| 331 |
+
|
| 332 |
+
如果问题要求解释,提供详细解释。
|
| 333 |
+
如果要求例子,提供真实有用的例子。
|
| 334 |
+
如果要求澄清,用简单术语解释概念。
|
| 335 |
+
"""
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
instructions = language_instructions.get(ui_language, language_instructions['en'])
|
| 339 |
+
|
| 340 |
+
context_parts.append(instructions)
|
| 341 |
+
|
| 342 |
+
return "\n".join(context_parts)
|
| 343 |
+
|
| 344 |
+
def _get_ai_response(self, context: str, ui_language: str) -> Tuple[Optional[str], Optional[str], Optional[str]]:
|
| 345 |
+
"""Get AI response using multiple AI services with fallback
|
| 346 |
+
|
| 347 |
+
Returns:
|
| 348 |
+
Tuple of (response_text, error_message, model_used)
|
| 349 |
+
"""
|
| 350 |
+
|
| 351 |
+
# Model 1: Try Gemini first
|
| 352 |
+
try:
|
| 353 |
+
if hasattr(self.translator, 'model') and self.translator.model:
|
| 354 |
+
response = self.translator.model.generate_content(context)
|
| 355 |
+
if response and hasattr(response, 'text') and response.text:
|
| 356 |
+
self.model_usage_stats['Gemini AI'] += 1
|
| 357 |
+
return response.text.strip(), None, "Gemini AI"
|
| 358 |
+
except Exception as e:
|
| 359 |
+
error_msg = str(e)
|
| 360 |
+
print(f"Gemini AI failed: {error_msg}") # Debug log
|
| 361 |
+
# Continue to next model instead of returning error immediately
|
| 362 |
+
|
| 363 |
+
# Model 2: Try Groq
|
| 364 |
+
try:
|
| 365 |
+
if hasattr(self.translator, '_groq_complete'):
|
| 366 |
+
response, error = self.translator._groq_complete(context)
|
| 367 |
+
if response and response.strip():
|
| 368 |
+
self.model_usage_stats['Groq AI'] += 1
|
| 369 |
+
return response.strip(), None, "Groq AI"
|
| 370 |
+
print(f"Groq failed: {error}") # Debug log
|
| 371 |
+
except Exception as e:
|
| 372 |
+
print(f"Groq exception: {str(e)}") # Debug log
|
| 373 |
+
|
| 374 |
+
# Model 3: Try OpenRouter
|
| 375 |
+
try:
|
| 376 |
+
if hasattr(self.translator, '_openrouter_complete'):
|
| 377 |
+
response, error = self.translator._openrouter_complete(context)
|
| 378 |
+
if response and response.strip():
|
| 379 |
+
self.model_usage_stats['OpenRouter AI'] += 1
|
| 380 |
+
return response.strip(), None, "OpenRouter AI"
|
| 381 |
+
print(f"OpenRouter failed: {error}") # Debug log
|
| 382 |
+
except Exception as e:
|
| 383 |
+
print(f"OpenRouter exception: {str(e)}") # Debug log
|
| 384 |
+
|
| 385 |
+
# Fallback: Simple rule-based response
|
| 386 |
+
simple_response, _ = self._generate_simple_response(context, ui_language)
|
| 387 |
+
self.model_usage_stats['Simple Response'] += 1
|
| 388 |
+
return simple_response, None, "Simple Response (AI services unavailable)"
|
| 389 |
+
|
| 390 |
+
def _try_fallback_services(self, context: str, ui_language: str) -> Tuple[Optional[str], Optional[str]]:
|
| 391 |
+
"""Try fallback AI services when Gemini is unavailable"""
|
| 392 |
+
|
| 393 |
+
# Try OpenRouter (free models)
|
| 394 |
+
if hasattr(self.translator, '_openrouter_complete'):
|
| 395 |
+
try:
|
| 396 |
+
response, error = self.translator._openrouter_complete(context)
|
| 397 |
+
if response:
|
| 398 |
+
return response.strip(), None
|
| 399 |
+
except Exception:
|
| 400 |
+
pass
|
| 401 |
+
|
| 402 |
+
# Try Groq (free tier)
|
| 403 |
+
if hasattr(self.translator, '_groq_complete'):
|
| 404 |
+
try:
|
| 405 |
+
response, error = self.translator._groq_complete(context)
|
| 406 |
+
if response:
|
| 407 |
+
return response.strip(), None
|
| 408 |
+
except Exception:
|
| 409 |
+
pass
|
| 410 |
+
|
| 411 |
+
# Fallback to simple rule-based responses
|
| 412 |
+
return self._generate_simple_response(context, ui_language)
|
| 413 |
+
|
| 414 |
+
def _generate_simple_response(self, context: str, ui_language: str) -> Tuple[Optional[str], Optional[str]]:
|
| 415 |
+
"""Generate simple rule-based response when AI services are unavailable"""
|
| 416 |
+
|
| 417 |
+
# Extract the question from context
|
| 418 |
+
lines = context.split('\n')
|
| 419 |
+
question = ""
|
| 420 |
+
selected_text = ""
|
| 421 |
+
|
| 422 |
+
for line in lines:
|
| 423 |
+
if line.startswith('Current question:'):
|
| 424 |
+
question = line.replace('Current question:', '').strip()
|
| 425 |
+
elif line.startswith('"') and line.endswith('"'):
|
| 426 |
+
selected_text = line.strip('"')
|
| 427 |
+
|
| 428 |
+
if not question or not selected_text:
|
| 429 |
+
return None, "Could not process question"
|
| 430 |
+
|
| 431 |
+
# Simple rule-based responses
|
| 432 |
+
if ui_language == 'ar':
|
| 433 |
+
responses = self._get_arabic_simple_responses(question, selected_text)
|
| 434 |
+
else:
|
| 435 |
+
responses = self._get_english_simple_responses(question, selected_text)
|
| 436 |
+
|
| 437 |
+
return responses
|
| 438 |
+
|
| 439 |
+
def _get_arabic_simple_responses(self, question: str, selected_text: str) -> Tuple[str, None]:
|
| 440 |
+
"""Generate simple Arabic responses based on question patterns"""
|
| 441 |
+
|
| 442 |
+
question_lower = question.lower()
|
| 443 |
+
|
| 444 |
+
if any(word in question_lower for word in ['اشرح', 'شرح', 'وضح']):
|
| 445 |
+
response = f"""بناءً على النص المحدد:
|
| 446 |
+
"{selected_text}"
|
| 447 |
+
|
| 448 |
+
هذا النص يتحدث عن موضوع مهم يحتاج إلى فهم عميق. النقاط الرئيسية تشمل المفاهيم والأفكار المطروحة في النص.
|
| 449 |
+
|
| 450 |
+
للحصول على شرح أكثر تفصيلاً، يُنصح بمراجعة مصادر إضافية أو طرح أسئلة أكثر تحديداً.
|
| 451 |
+
|
| 452 |
+
ملاحظة: هذه إجابة مبسطة بسبب عدم توفر خدمة الذكاء الاصطناعي حالياً."""
|
| 453 |
+
|
| 454 |
+
elif any(word in question_lower for word in ['أمثلة', 'مثال', 'تطبيق']):
|
| 455 |
+
response = f"""أمثلة على النص المحدد:
|
| 456 |
+
"{selected_text}"
|
| 457 |
+
|
| 458 |
+
يمكن تطبيق هذا المفهوم في عدة مجالات:
|
| 459 |
+
• في الحياة العملية
|
| 460 |
+
• في الدراسة والتعلم
|
| 461 |
+
• في المشاريع والأعمال
|
| 462 |
+
|
| 463 |
+
للحصول على أمثلة أكثر تفصيلاً، يُنصح بالبحث في مصادر متخصصة.
|
| 464 |
+
|
| 465 |
+
ملاحظة: هذه إجابة مبسطة بسبب عدم توفر خدمة الذكاء الاصطناعي حالياً."""
|
| 466 |
+
|
| 467 |
+
elif any(word in question_lower for word in ['معنى', 'تعريف', 'ما هو']):
|
| 468 |
+
response = f"""معنى النص المحدد:
|
| 469 |
+
"{selected_text}"
|
| 470 |
+
|
| 471 |
+
هذا النص يشير إلى مفهوم أو فكرة معينة تحتاج إلى تفسير. المعنى العام يتعلق بالموضوع المطروح في السياق.
|
| 472 |
+
|
| 473 |
+
للحصول على تعريف أكثر دقة، يُنصح بمراجعة المصادر المتخصصة.
|
| 474 |
+
|
| 475 |
+
ملاحظة: هذه إجابة مبسطة بسبب عدم توفر خدمة الذكاء الاصطناعي حالياً."""
|
| 476 |
+
|
| 477 |
+
else:
|
| 478 |
+
response = f"""بخصوص سؤالك حول:
|
| 479 |
+
"{selected_text}"
|
| 480 |
+
|
| 481 |
+
هذا موضوع مهم يستحق الدراسة والتفكير. النص المحدد يحتوي على معلومات قيمة يمكن الاستفادة منها.
|
| 482 |
+
|
| 483 |
+
للحصول على إجابة أكثر تفصيلاً، يُنصح بـ:
|
| 484 |
+
• مراجعة مصادر إضافية
|
| 485 |
+
• طرح أسئلة أكثر تحديداً
|
| 486 |
+
• البحث في المراجع المتخصصة
|
| 487 |
+
|
| 488 |
+
ملاحظة: هذه إجابة مبسطة بسبب عدم توفر خدمة الذكاء الاصطناعي حالياً."""
|
| 489 |
+
|
| 490 |
+
return response, None
|
| 491 |
+
|
| 492 |
+
def _get_english_simple_responses(self, question: str, selected_text: str) -> Tuple[str, None]:
|
| 493 |
+
"""Generate simple English responses based on question patterns"""
|
| 494 |
+
|
| 495 |
+
question_lower = question.lower()
|
| 496 |
+
|
| 497 |
+
if any(word in question_lower for word in ['explain', 'clarify', 'describe']):
|
| 498 |
+
response = f"""Based on the selected text:
|
| 499 |
+
"{selected_text}"
|
| 500 |
+
|
| 501 |
+
This text discusses an important topic that requires deep understanding. The main points include the concepts and ideas presented in the text.
|
| 502 |
+
|
| 503 |
+
For a more detailed explanation, it's recommended to consult additional sources or ask more specific questions.
|
| 504 |
+
|
| 505 |
+
Note: This is a simplified response due to AI service being temporarily unavailable."""
|
| 506 |
+
|
| 507 |
+
elif any(word in question_lower for word in ['example', 'application', 'use']):
|
| 508 |
+
response = f"""Examples related to the selected text:
|
| 509 |
+
"{selected_text}"
|
| 510 |
+
|
| 511 |
+
This concept can be applied in several areas:
|
| 512 |
+
• In practical life
|
| 513 |
+
• In study and learning
|
| 514 |
+
• In projects and work
|
| 515 |
+
|
| 516 |
+
For more detailed examples, it's recommended to search specialized sources.
|
| 517 |
+
|
| 518 |
+
Note: This is a simplified response due to AI service being temporarily unavailable."""
|
| 519 |
+
|
| 520 |
+
elif any(word in question_lower for word in ['mean', 'definition', 'what is']):
|
| 521 |
+
response = f"""Meaning of the selected text:
|
| 522 |
+
"{selected_text}"
|
| 523 |
+
|
| 524 |
+
This text refers to a specific concept or idea that needs interpretation. The general meaning relates to the topic presented in the context.
|
| 525 |
+
|
| 526 |
+
For a more precise definition, it's recommended to consult specialized sources.
|
| 527 |
+
|
| 528 |
+
Note: This is a simplified response due to AI service being temporarily unavailable."""
|
| 529 |
+
|
| 530 |
+
else:
|
| 531 |
+
response = f"""Regarding your question about:
|
| 532 |
+
"{selected_text}"
|
| 533 |
+
|
| 534 |
+
This is an important topic worth studying and thinking about. The selected text contains valuable information that can be beneficial.
|
| 535 |
+
|
| 536 |
+
For a more detailed answer, it's recommended to:
|
| 537 |
+
• Consult additional sources
|
| 538 |
+
• Ask more specific questions
|
| 539 |
+
• Search specialized references
|
| 540 |
+
|
| 541 |
+
Note: This is a simplified response due to AI service being temporarily unavailable."""
|
| 542 |
+
|
| 543 |
+
return response, None
|
| 544 |
+
|
| 545 |
+
def format_ai_response(self, response: str, ui_language: str) -> str:
|
| 546 |
+
"""Format AI response for better display"""
|
| 547 |
+
|
| 548 |
+
# Clean up response
|
| 549 |
+
response = response.strip()
|
| 550 |
+
|
| 551 |
+
# Remove markdown artifacts
|
| 552 |
+
response = response.replace('**', '')
|
| 553 |
+
response = response.replace('```', '')
|
| 554 |
+
response = response.replace('`', '')
|
| 555 |
+
|
| 556 |
+
# Remove extra whitespace
|
| 557 |
+
lines = [line.strip() for line in response.split('\n')]
|
| 558 |
+
response = '\n'.join(line for line in lines if line)
|
| 559 |
+
|
| 560 |
+
return response
|
| 561 |
+
|
| 562 |
+
def get_conversation_history(self, session_id: str) -> Optional[QuestionSession]:
|
| 563 |
+
"""Get conversation history for a specific session"""
|
| 564 |
+
return self.conversation_history.get(session_id)
|
| 565 |
+
|
| 566 |
+
def clear_conversation(self, session_id: str) -> bool:
|
| 567 |
+
"""Clear conversation history for a specific session"""
|
| 568 |
+
if session_id in self.conversation_history:
|
| 569 |
+
del self.conversation_history[session_id]
|
| 570 |
+
return True
|
| 571 |
+
return False
|
| 572 |
+
|
| 573 |
+
def get_all_sessions(self) -> Dict[str, QuestionSession]:
|
| 574 |
+
"""Get all active question sessions"""
|
| 575 |
+
return self.conversation_history.copy()
|
| 576 |
+
|
| 577 |
+
def get_model_usage_stats(self) -> Dict[str, int]:
|
| 578 |
+
"""Get usage statistics for different AI models"""
|
| 579 |
+
return self.model_usage_stats.copy()
|
| 580 |
+
|
| 581 |
+
def check_model_availability(self) -> Dict[str, Dict[str, Any]]:
|
| 582 |
+
"""Check availability status of all AI models"""
|
| 583 |
+
models_status = {}
|
| 584 |
+
|
| 585 |
+
# Check Gemini
|
| 586 |
+
try:
|
| 587 |
+
if hasattr(self.translator, 'model') and self.translator.model:
|
| 588 |
+
# Try a minimal test
|
| 589 |
+
test_response = self.translator.model.generate_content("Hi")
|
| 590 |
+
models_status['Gemini AI'] = {
|
| 591 |
+
'status': 'available',
|
| 592 |
+
'icon': '✅',
|
| 593 |
+
'message': 'متاح' if st.session_state.get('language', 'ar') == 'ar' else 'Available',
|
| 594 |
+
'color': 'green'
|
| 595 |
+
}
|
| 596 |
+
else:
|
| 597 |
+
models_status['Gemini AI'] = {
|
| 598 |
+
'status': 'unavailable',
|
| 599 |
+
'icon': '❌',
|
| 600 |
+
'message': 'غير متاح' if st.session_state.get('language', 'ar') == 'ar' else 'Unavailable',
|
| 601 |
+
'color': 'red'
|
| 602 |
+
}
|
| 603 |
+
except Exception as e:
|
| 604 |
+
error_str = str(e)
|
| 605 |
+
if "429" in error_str or "quota" in error_str.lower():
|
| 606 |
+
models_status['Gemini AI'] = {
|
| 607 |
+
'status': 'quota_exceeded',
|
| 608 |
+
'icon': '⚠️',
|
| 609 |
+
'message': 'انتهت الحصة' if st.session_state.get('language', 'ar') == 'ar' else 'Quota exceeded',
|
| 610 |
+
'color': 'orange'
|
| 611 |
+
}
|
| 612 |
+
else:
|
| 613 |
+
models_status['Gemini AI'] = {
|
| 614 |
+
'status': 'error',
|
| 615 |
+
'icon': '❌',
|
| 616 |
+
'message': 'خطأ مؤقت' if st.session_state.get('language', 'ar') == 'ar' else 'Temporary error',
|
| 617 |
+
'color': 'red'
|
| 618 |
+
}
|
| 619 |
+
|
| 620 |
+
# Check Groq
|
| 621 |
+
try:
|
| 622 |
+
if hasattr(self.translator, '_groq_complete') and hasattr(self.translator, 'groq_api_key') and self.translator.groq_api_key:
|
| 623 |
+
models_status['Groq AI'] = {
|
| 624 |
+
'status': 'available',
|
| 625 |
+
'icon': '✅',
|
| 626 |
+
'message': 'متاح' if st.session_state.get('language', 'ar') == 'ar' else 'Available',
|
| 627 |
+
'color': 'green'
|
| 628 |
+
}
|
| 629 |
+
else:
|
| 630 |
+
models_status['Groq AI'] = {
|
| 631 |
+
'status': 'not_configured',
|
| 632 |
+
'icon': '⚙️',
|
| 633 |
+
'message': 'غير مُعد' if st.session_state.get('language', 'ar') == 'ar' else 'Not configured',
|
| 634 |
+
'color': 'gray'
|
| 635 |
+
}
|
| 636 |
+
except Exception:
|
| 637 |
+
models_status['Groq AI'] = {
|
| 638 |
+
'status': 'error',
|
| 639 |
+
'icon': '❌',
|
| 640 |
+
'message': 'خطأ' if st.session_state.get('language', 'ar') == 'ar' else 'Error',
|
| 641 |
+
'color': 'red'
|
| 642 |
+
}
|
| 643 |
+
|
| 644 |
+
# Check OpenRouter
|
| 645 |
+
try:
|
| 646 |
+
if hasattr(self.translator, '_openrouter_complete') and hasattr(self.translator, 'openrouter_api_key') and self.translator.openrouter_api_key:
|
| 647 |
+
models_status['OpenRouter AI'] = {
|
| 648 |
+
'status': 'available',
|
| 649 |
+
'icon': '✅',
|
| 650 |
+
'message': 'متاح' if st.session_state.get('language', 'ar') == 'ar' else 'Available',
|
| 651 |
+
'color': 'green'
|
| 652 |
+
}
|
| 653 |
+
else:
|
| 654 |
+
models_status['OpenRouter AI'] = {
|
| 655 |
+
'status': 'not_configured',
|
| 656 |
+
'icon': '⚙️',
|
| 657 |
+
'message': 'غير مُعد' if st.session_state.get('language', 'ar') == 'ar' else 'Not configured',
|
| 658 |
+
'color': 'gray'
|
| 659 |
+
}
|
| 660 |
+
except Exception:
|
| 661 |
+
models_status['OpenRouter AI'] = {
|
| 662 |
+
'status': 'error',
|
| 663 |
+
'icon': '❌',
|
| 664 |
+
'message': 'خطأ' if st.session_state.get('language', 'ar') == 'ar' else 'Error',
|
| 665 |
+
'color': 'red'
|
| 666 |
+
}
|
| 667 |
+
|
| 668 |
+
# Simple Response is always available
|
| 669 |
+
models_status['Simple Response'] = {
|
| 670 |
+
'status': 'available',
|
| 671 |
+
'icon': '🛡️',
|
| 672 |
+
'message': 'متاح دائماً' if st.session_state.get('language', 'ar') == 'ar' else 'Always available',
|
| 673 |
+
'color': 'blue'
|
| 674 |
+
}
|
| 675 |
+
|
| 676 |
+
return models_status
|
| 677 |
+
|
| 678 |
+
def get_ai_response_with_model(self, context: str, ui_language: str, preferred_model: str = 'auto') -> Tuple[Optional[str], Optional[str], Optional[str]]:
|
| 679 |
+
"""Get AI response using a specific model or auto-selection"""
|
| 680 |
+
|
| 681 |
+
if preferred_model == 'auto':
|
| 682 |
+
return self._get_ai_response(context, ui_language)
|
| 683 |
+
|
| 684 |
+
# Try specific model first
|
| 685 |
+
if preferred_model == 'Gemini AI':
|
| 686 |
+
try:
|
| 687 |
+
if hasattr(self.translator, 'model') and self.translator.model:
|
| 688 |
+
response = self.translator.model.generate_content(context)
|
| 689 |
+
if response and hasattr(response, 'text') and response.text:
|
| 690 |
+
self.model_usage_stats['Gemini AI'] += 1
|
| 691 |
+
return response.text.strip(), None, "Gemini AI"
|
| 692 |
+
except Exception as e:
|
| 693 |
+
return None, f"Gemini AI error: {str(e)}", None
|
| 694 |
+
|
| 695 |
+
elif preferred_model == 'Groq AI':
|
| 696 |
+
try:
|
| 697 |
+
if hasattr(self.translator, '_groq_complete'):
|
| 698 |
+
response, error = self.translator._groq_complete(context)
|
| 699 |
+
if response and response.strip():
|
| 700 |
+
self.model_usage_stats['Groq AI'] += 1
|
| 701 |
+
return response.strip(), None, "Groq AI"
|
| 702 |
+
else:
|
| 703 |
+
return None, f"Groq AI error: {error}", None
|
| 704 |
+
except Exception as e:
|
| 705 |
+
return None, f"Groq AI error: {str(e)}", None
|
| 706 |
+
|
| 707 |
+
elif preferred_model == 'OpenRouter AI':
|
| 708 |
+
try:
|
| 709 |
+
if hasattr(self.translator, '_openrouter_complete'):
|
| 710 |
+
response, error = self.translator._openrouter_complete(context)
|
| 711 |
+
if response and response.strip():
|
| 712 |
+
self.model_usage_stats['OpenRouter AI'] += 1
|
| 713 |
+
return response.strip(), None, "OpenRouter AI"
|
| 714 |
+
else:
|
| 715 |
+
return None, f"OpenRouter AI error: {error}", None
|
| 716 |
+
except Exception as e:
|
| 717 |
+
return None, f"OpenRouter AI error: {str(e)}", None
|
| 718 |
+
|
| 719 |
+
elif preferred_model == 'Simple Response':
|
| 720 |
+
simple_response, _ = self._generate_simple_response(context, ui_language)
|
| 721 |
+
self.model_usage_stats['Simple Response'] += 1
|
| 722 |
+
return simple_response, None, "Simple Response"
|
| 723 |
+
|
| 724 |
+
# If preferred model fails, fall back to auto-selection
|
| 725 |
+
return self._get_ai_response(context, ui_language)
|
| 726 |
+
|
| 727 |
+
def format_conversation_for_export(self, session_id: str, ui_language: str = 'ar') -> Optional[str]:
|
| 728 |
+
"""Format conversation for export/copying"""
|
| 729 |
+
|
| 730 |
+
session = self.conversation_history.get(session_id)
|
| 731 |
+
if not session:
|
| 732 |
+
return None
|
| 733 |
+
|
| 734 |
+
export_lines = []
|
| 735 |
+
|
| 736 |
+
# Header
|
| 737 |
+
if ui_language == 'ar':
|
| 738 |
+
export_lines.append("محادثة الذكاء الاصطناعي")
|
| 739 |
+
export_lines.append(f"النص المحدد: {session.selected_text}")
|
| 740 |
+
export_lines.append(f"التاريخ: {session.created_at.strftime('%Y-%m-%d %H:%M:%S')}")
|
| 741 |
+
else:
|
| 742 |
+
export_lines.append("AI Conversation")
|
| 743 |
+
export_lines.append(f"Selected Text: {session.selected_text}")
|
| 744 |
+
export_lines.append(f"Date: {session.created_at.strftime('%Y-%m-%d %H:%M:%S')}")
|
| 745 |
+
|
| 746 |
+
export_lines.append("=" * 50)
|
| 747 |
+
export_lines.append("")
|
| 748 |
+
|
| 749 |
+
# Q&A pairs
|
| 750 |
+
for i, qa in enumerate(session.conversation, 1):
|
| 751 |
+
if ui_language == 'ar':
|
| 752 |
+
export_lines.append(f"السؤال {i}: {qa.question}")
|
| 753 |
+
export_lines.append(f"الإجابة {i}: {qa.answer}")
|
| 754 |
+
else:
|
| 755 |
+
export_lines.append(f"Question {i}: {qa.question}")
|
| 756 |
+
export_lines.append(f"Answer {i}: {qa.answer}")
|
| 757 |
+
|
| 758 |
+
export_lines.append("-" * 30)
|
| 759 |
+
export_lines.append("")
|
| 760 |
+
|
| 761 |
+
return "\n".join(export_lines)
|
| 762 |
+
|
| 763 |
+
# Global instance
|
| 764 |
+
ai_question_engine = None
|
| 765 |
+
|
| 766 |
+
def get_ai_question_engine():
|
| 767 |
+
"""Get or create AI question engine instance"""
|
| 768 |
+
global ai_question_engine
|
| 769 |
+
if ai_question_engine is None:
|
| 770 |
+
from translator import get_translator
|
| 771 |
+
translator = get_translator()
|
| 772 |
+
ai_question_engine = AIQuestionEngine(translator)
|
| 773 |
+
return ai_question_engine
|
app.py
ADDED
|
@@ -0,0 +1,1812 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app.py - Refactored to eliminate recorder_server.py dependency
|
| 2 |
+
|
| 3 |
+
import streamlit as st
|
| 4 |
+
import os
|
| 5 |
+
import tempfile
|
| 6 |
+
import json
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
import time
|
| 9 |
+
import traceback
|
| 10 |
+
import streamlit.components.v1 as components
|
| 11 |
+
import hashlib
|
| 12 |
+
from datetime import datetime
|
| 13 |
+
# from st_audiorec import st_audiorec # Import the new recorder component - OLD
|
| 14 |
+
# Reduce metrics/usage writes that can cause permission errors on hosted environments
|
| 15 |
+
try:
|
| 16 |
+
st.set_option('browser.gatherUsageStats', False)
|
| 17 |
+
except Exception:
|
| 18 |
+
pass
|
| 19 |
+
|
| 20 |
+
# Robust component declaration: prefer local build, else fall back to pip package
|
| 21 |
+
parent_dir = os.path.dirname(os.path.abspath(__file__))
|
| 22 |
+
build_dir = os.path.join(parent_dir, "custom_components/st-audiorec/st_audiorec/frontend/build")
|
| 23 |
+
|
| 24 |
+
def st_audiorec(key=None):
|
| 25 |
+
"""Return audio recorder component value, trying local build first, then pip package fallback."""
|
| 26 |
+
try:
|
| 27 |
+
if os.path.isdir(build_dir):
|
| 28 |
+
_component_func = components.declare_component("st_audiorec", path=build_dir)
|
| 29 |
+
return _component_func(key=key, default=0)
|
| 30 |
+
# Fallback to pip-installed component if available
|
| 31 |
+
try:
|
| 32 |
+
from st_audiorec import st_audiorec as st_audiorec_pkg
|
| 33 |
+
return st_audiorec_pkg(key=key)
|
| 34 |
+
except Exception:
|
| 35 |
+
st.warning("Audio recorder component is unavailable on this deployment (missing local build and pip fallback).")
|
| 36 |
+
return None
|
| 37 |
+
except Exception:
|
| 38 |
+
# Final safety net
|
| 39 |
+
st.warning("Failed to initialize audio recorder component.")
|
| 40 |
+
return None
|
| 41 |
+
|
| 42 |
+
# --- Critical Imports and Initial Checks ---
|
| 43 |
+
AUDIO_PROCESSOR_CLASS = None
|
| 44 |
+
IMPORT_ERROR_TRACEBACK = None
|
| 45 |
+
try:
|
| 46 |
+
from audio_processor import AudioProcessor
|
| 47 |
+
AUDIO_PROCESSOR_CLASS = AudioProcessor
|
| 48 |
+
except Exception:
|
| 49 |
+
IMPORT_ERROR_TRACEBACK = traceback.format_exc()
|
| 50 |
+
|
| 51 |
+
from video_generator import VideoGenerator
|
| 52 |
+
from mp3_embedder import MP3Embedder
|
| 53 |
+
from utils import format_timestamp
|
| 54 |
+
from translator import get_translator, UI_TRANSLATIONS
|
| 55 |
+
from exporter import BroadcastExporter, ExportConfig
|
| 56 |
+
from google_docs_config import google_docs_manager
|
| 57 |
+
from ai_questions import get_ai_question_engine, TextSelection
|
| 58 |
+
from style_fixes import apply_custom_styling, create_broadcast_bubble, create_white_container, create_processing_result_container
|
| 59 |
+
import requests
|
| 60 |
+
from dotenv import load_dotenv
|
| 61 |
+
|
| 62 |
+
# --- API Key Check ---
|
| 63 |
+
def check_api_key():
|
| 64 |
+
"""Check for Gemini API key and display instructions if not found."""
|
| 65 |
+
load_dotenv()
|
| 66 |
+
if not os.getenv("GEMINI_API_KEY"):
|
| 67 |
+
st.error("🔴 FATAL ERROR: GEMINI_API_KEY is not set!")
|
| 68 |
+
st.info("To fix this, please follow these steps:")
|
| 69 |
+
st.markdown("""
|
| 70 |
+
1. **Find the file named `.env.example`** in the `syncmaster2` directory.
|
| 71 |
+
2. **Rename it to `.env`**.
|
| 72 |
+
3. **Open the `.env` file** with a text editor.
|
| 73 |
+
4. **Get your free API key** from [Google AI Studio](https://aistudio.google.com/app/apikey).
|
| 74 |
+
5. **Paste your key** into the file, replacing `"PASTE_YOUR_GEMINI_API_KEY_HERE"`.
|
| 75 |
+
6. **Save the file and restart the application.**
|
| 76 |
+
""")
|
| 77 |
+
return False
|
| 78 |
+
return True
|
| 79 |
+
|
| 80 |
+
# --- Summary Helper (robust to cached translator without summarize_text) ---
|
| 81 |
+
def generate_summary(text: str, target_language: str = 'ar'):
|
| 82 |
+
"""Generate a concise summary in target_language, with graceful fallback.
|
| 83 |
+
|
| 84 |
+
If summarize_text is unavailable (cached instance), fall back to Arabic summary
|
| 85 |
+
then translate to the target language if needed.
|
| 86 |
+
"""
|
| 87 |
+
tr = get_translator()
|
| 88 |
+
try:
|
| 89 |
+
if hasattr(tr, 'summarize_text') and callable(getattr(tr, 'summarize_text')):
|
| 90 |
+
s, err = tr.summarize_text(text or '', target_language=target_language)
|
| 91 |
+
if s:
|
| 92 |
+
return s, None
|
| 93 |
+
# Fallback path: Arabic summary first
|
| 94 |
+
s_ar, err_ar = tr.summarize_text_arabic(text or '')
|
| 95 |
+
if target_language and target_language != 'ar' and s_ar:
|
| 96 |
+
tx, err_tx = tr.translate_text(s_ar, target_language=target_language)
|
| 97 |
+
if tx:
|
| 98 |
+
return tx, None
|
| 99 |
+
return s_ar, err_tx
|
| 100 |
+
return s_ar, err_ar
|
| 101 |
+
except Exception as e:
|
| 102 |
+
return None, str(e)
|
| 103 |
+
|
| 104 |
+
# --- Page Configuration ---
|
| 105 |
+
st.set_page_config(
|
| 106 |
+
page_title="SyncMaster - AI Audio-Text Synchronization",
|
| 107 |
+
page_icon="🎵",
|
| 108 |
+
layout="wide"
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
# --- Browser Console Logging Utility ---
|
| 112 |
+
def log_to_browser_console(messages):
|
| 113 |
+
"""Injects JavaScript to log messages to the browser's console."""
|
| 114 |
+
if isinstance(messages, str):
|
| 115 |
+
messages = [messages]
|
| 116 |
+
escaped_messages = [json.dumps(str(msg)) for msg in messages]
|
| 117 |
+
js_code = f"""
|
| 118 |
+
<script>
|
| 119 |
+
(function() {{
|
| 120 |
+
const logs = [{', '.join(escaped_messages)}];
|
| 121 |
+
console.group("Backend Logs from SyncMaster");
|
| 122 |
+
logs.forEach(log => {{
|
| 123 |
+
const content = String(log);
|
| 124 |
+
if (content.includes('--- ERROR') || content.includes('--- FATAL')) {{
|
| 125 |
+
console.error(log);
|
| 126 |
+
}} else if (content.includes('--- WARNING')) {{
|
| 127 |
+
console.warn(log);
|
| 128 |
+
}} else if (content.includes('--- DEBUG')) {{
|
| 129 |
+
console.debug(log);
|
| 130 |
+
}} else {{
|
| 131 |
+
console.log(log);
|
| 132 |
+
}}
|
| 133 |
+
}});
|
| 134 |
+
console.groupEnd();
|
| 135 |
+
}})();
|
| 136 |
+
</script>
|
| 137 |
+
"""
|
| 138 |
+
components.html(js_code, height=0, scrolling=False)
|
| 139 |
+
|
| 140 |
+
# --- AI Models Reset Function ---
|
| 141 |
+
def reset_ai_models():
|
| 142 |
+
"""Reset all AI models and clear cache completely"""
|
| 143 |
+
|
| 144 |
+
# Clear ALL session state keys that might contain cached AI instances
|
| 145 |
+
keys_to_clear = []
|
| 146 |
+
for key in list(st.session_state.keys()):
|
| 147 |
+
if any(term in key.lower() for term in ['translator', 'ai', 'question', 'model', 'engine', 'processing']):
|
| 148 |
+
keys_to_clear.append(key)
|
| 149 |
+
|
| 150 |
+
for key in keys_to_clear:
|
| 151 |
+
del st.session_state[key]
|
| 152 |
+
|
| 153 |
+
# Force reload environment variables
|
| 154 |
+
load_dotenv(override=True)
|
| 155 |
+
|
| 156 |
+
# Clear Python module cache completely
|
| 157 |
+
import sys
|
| 158 |
+
import importlib
|
| 159 |
+
|
| 160 |
+
modules_to_reload = ['translator', 'ai_questions', 'exporter']
|
| 161 |
+
for module_name in modules_to_reload:
|
| 162 |
+
if module_name in sys.modules:
|
| 163 |
+
try:
|
| 164 |
+
# Delete from sys.modules first
|
| 165 |
+
del sys.modules[module_name]
|
| 166 |
+
except:
|
| 167 |
+
pass
|
| 168 |
+
|
| 169 |
+
# Clear any global instances
|
| 170 |
+
try:
|
| 171 |
+
import translator
|
| 172 |
+
if hasattr(translator, 'translator_instance'):
|
| 173 |
+
translator.translator_instance = None
|
| 174 |
+
except:
|
| 175 |
+
pass
|
| 176 |
+
|
| 177 |
+
try:
|
| 178 |
+
import ai_questions
|
| 179 |
+
if hasattr(ai_questions, 'ai_question_engine'):
|
| 180 |
+
ai_questions.ai_question_engine = None
|
| 181 |
+
except:
|
| 182 |
+
pass
|
| 183 |
+
|
| 184 |
+
# --- Session State Initialization ---
|
| 185 |
+
def initialize_session_state():
|
| 186 |
+
"""Initializes the session state variables if they don't exist."""
|
| 187 |
+
if 'step' not in st.session_state:
|
| 188 |
+
st.session_state.step = 1
|
| 189 |
+
if 'audio_data' not in st.session_state:
|
| 190 |
+
st.session_state.audio_data = None
|
| 191 |
+
if 'language' not in st.session_state:
|
| 192 |
+
st.session_state.language = 'en'
|
| 193 |
+
if 'enable_translation' not in st.session_state:
|
| 194 |
+
st.session_state.enable_translation = True
|
| 195 |
+
if 'target_language' not in st.session_state:
|
| 196 |
+
st.session_state.target_language = 'ar'
|
| 197 |
+
if 'transcription_data' not in st.session_state:
|
| 198 |
+
st.session_state.transcription_data = None
|
| 199 |
+
if 'edited_text' not in st.session_state:
|
| 200 |
+
st.session_state.edited_text = ""
|
| 201 |
+
if 'video_style' not in st.session_state:
|
| 202 |
+
st.session_state.video_style = {
|
| 203 |
+
'animation_style': 'Karaoke Style', 'text_color': '#FFFFFF',
|
| 204 |
+
'highlight_color': '#FFD700', 'background_color': '#000000',
|
| 205 |
+
'font_family': 'Arial', 'font_size': 48
|
| 206 |
+
}
|
| 207 |
+
if 'new_recording' not in st.session_state:
|
| 208 |
+
st.session_state.new_recording = None
|
| 209 |
+
# Transcript feed (prepend latest) and dedupe set
|
| 210 |
+
if 'transcript_feed' not in st.session_state:
|
| 211 |
+
st.session_state.transcript_feed = [] # list of {id, ts, text}
|
| 212 |
+
if 'transcript_ids' not in st.session_state:
|
| 213 |
+
st.session_state.transcript_ids = set()
|
| 214 |
+
# Incremental broadcast state
|
| 215 |
+
if 'broadcast_segments' not in st.session_state:
|
| 216 |
+
st.session_state.broadcast_segments = [] # [{id, recording_id, start_ms, end_ms, checksum, text}]
|
| 217 |
+
if 'lastFetchedEnd_ms' not in st.session_state:
|
| 218 |
+
st.session_state.lastFetchedEnd_ms = 0
|
| 219 |
+
# Broadcast translation language (separate from general UI translation target)
|
| 220 |
+
if 'broadcast_translation_lang' not in st.session_state:
|
| 221 |
+
# Default broadcast translation target to Arabic
|
| 222 |
+
st.session_state.broadcast_translation_lang = 'ar'
|
| 223 |
+
if 'summary_language' not in st.session_state:
|
| 224 |
+
# Default summary language to Arabic
|
| 225 |
+
st.session_state.summary_language = 'ar'
|
| 226 |
+
# Auto-generate Arabic summary toggle
|
| 227 |
+
if 'auto_generate_summary' not in st.session_state:
|
| 228 |
+
st.session_state.auto_generate_summary = True
|
| 229 |
+
# Export functionality state
|
| 230 |
+
if 'export_timestamp' not in st.session_state:
|
| 231 |
+
st.session_state.export_timestamp = None
|
| 232 |
+
if 'show_export_modal' not in st.session_state:
|
| 233 |
+
st.session_state.show_export_modal = False
|
| 234 |
+
if 'export_format' not in st.session_state:
|
| 235 |
+
st.session_state.export_format = 'word'
|
| 236 |
+
# AI Questions functionality state
|
| 237 |
+
if 'selected_text' not in st.session_state:
|
| 238 |
+
st.session_state.selected_text = None
|
| 239 |
+
if 'selected_segment_id' not in st.session_state:
|
| 240 |
+
st.session_state.selected_segment_id = None
|
| 241 |
+
if 'show_question_modal' not in st.session_state:
|
| 242 |
+
st.session_state.show_question_modal = False
|
| 243 |
+
if 'current_question_session' not in st.session_state:
|
| 244 |
+
st.session_state.current_question_session = None
|
| 245 |
+
if 'preferred_ai_model' not in st.session_state:
|
| 246 |
+
st.session_state.preferred_ai_model = 'auto'
|
| 247 |
+
if 'preferred_answer_language' not in st.session_state:
|
| 248 |
+
st.session_state.preferred_answer_language = 'auto'
|
| 249 |
+
# Background processing state
|
| 250 |
+
if 'processing_queue' not in st.session_state:
|
| 251 |
+
st.session_state.processing_queue = []
|
| 252 |
+
if 'processing_results' not in st.session_state:
|
| 253 |
+
st.session_state.processing_results = {}
|
| 254 |
+
if 'processing_status' not in st.session_state:
|
| 255 |
+
st.session_state.processing_status = {}
|
| 256 |
+
|
| 257 |
+
# --- Background Audio Processing Function ---
|
| 258 |
+
def queue_audio_processing(audio_bytes, original_filename="recorded_audio.wav"):
|
| 259 |
+
"""Queue audio for background processing"""
|
| 260 |
+
import uuid
|
| 261 |
+
|
| 262 |
+
# Generate unique ID for this processing task
|
| 263 |
+
task_id = str(uuid.uuid4())[:8]
|
| 264 |
+
|
| 265 |
+
# Add to processing queue
|
| 266 |
+
task = {
|
| 267 |
+
'id': task_id,
|
| 268 |
+
'audio_bytes': audio_bytes,
|
| 269 |
+
'filename': original_filename,
|
| 270 |
+
'timestamp': time.time(),
|
| 271 |
+
'status': 'queued'
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
st.session_state.processing_queue.append(task)
|
| 275 |
+
st.session_state.processing_status[task_id] = 'queued'
|
| 276 |
+
|
| 277 |
+
# Show immediate feedback
|
| 278 |
+
st.info(f"🔄 {'تم إضافة التسجيل للمعالجة' if st.session_state.language == 'ar' else 'Audio queued for processing'} (ID: {task_id})")
|
| 279 |
+
|
| 280 |
+
return task_id
|
| 281 |
+
|
| 282 |
+
def process_queued_audio():
|
| 283 |
+
"""Process queued audio in background"""
|
| 284 |
+
if not st.session_state.processing_queue:
|
| 285 |
+
return
|
| 286 |
+
|
| 287 |
+
# Process first item in queue
|
| 288 |
+
task = st.session_state.processing_queue[0]
|
| 289 |
+
task_id = task['id']
|
| 290 |
+
|
| 291 |
+
# Update status
|
| 292 |
+
st.session_state.processing_status[task_id] = 'processing'
|
| 293 |
+
task['status'] = 'processing'
|
| 294 |
+
|
| 295 |
+
# Show processing status
|
| 296 |
+
with st.status(f"🔄 {'معالجة التسجيل' if st.session_state.language == 'ar' else 'Processing audio'} {task_id}...", expanded=False):
|
| 297 |
+
try:
|
| 298 |
+
# Process the audio
|
| 299 |
+
result = run_audio_processing_sync(task['audio_bytes'], task['filename'])
|
| 300 |
+
|
| 301 |
+
if result:
|
| 302 |
+
# Store result
|
| 303 |
+
st.session_state.processing_results[task_id] = result
|
| 304 |
+
st.session_state.processing_status[task_id] = 'completed'
|
| 305 |
+
task['status'] = 'completed'
|
| 306 |
+
|
| 307 |
+
st.success(f"✅ {'تم الانتهاء من المعالجة' if st.session_state.language == 'ar' else 'Processing completed'} {task_id}")
|
| 308 |
+
else:
|
| 309 |
+
st.session_state.processing_status[task_id] = 'failed'
|
| 310 |
+
task['status'] = 'failed'
|
| 311 |
+
st.error(f"❌ {'فشلت المعالجة' if st.session_state.language == 'ar' else 'Processing failed'} {task_id}")
|
| 312 |
+
|
| 313 |
+
except Exception as e:
|
| 314 |
+
st.session_state.processing_status[task_id] = 'failed'
|
| 315 |
+
task['status'] = 'failed'
|
| 316 |
+
st.error(f"❌ {'خطأ في المعالجة' if st.session_state.language == 'ar' else 'Processing error'} {task_id}: {str(e)}")
|
| 317 |
+
|
| 318 |
+
# Remove from queue
|
| 319 |
+
st.session_state.processing_queue.pop(0)
|
| 320 |
+
|
| 321 |
+
# --- Centralized Audio Processing Function ---
|
| 322 |
+
def run_audio_processing(audio_bytes, original_filename="recorded_audio.wav"):
|
| 323 |
+
"""Main audio processing function with background support"""
|
| 324 |
+
|
| 325 |
+
# Check if background processing is enabled
|
| 326 |
+
if st.session_state.get('background_processing', True):
|
| 327 |
+
return queue_audio_processing(audio_bytes, original_filename)
|
| 328 |
+
else:
|
| 329 |
+
return run_audio_processing_sync(audio_bytes, original_filename)
|
| 330 |
+
|
| 331 |
+
def run_audio_processing_sync(audio_bytes, original_filename="recorded_audio.wav"):
|
| 332 |
+
"""
|
| 333 |
+
A single, robust function to handle all audio processing.
|
| 334 |
+
Takes audio bytes as input and returns the processed data.
|
| 335 |
+
"""
|
| 336 |
+
# This function is the classic, non-Custom path; ensure editor sections are enabled
|
| 337 |
+
st.session_state['_custom_active'] = False
|
| 338 |
+
if not audio_bytes:
|
| 339 |
+
st.error("No audio data provided to process.")
|
| 340 |
+
return
|
| 341 |
+
|
| 342 |
+
tmp_file_path = None
|
| 343 |
+
log_to_browser_console("--- INFO: Starting unified audio processing. ---")
|
| 344 |
+
|
| 345 |
+
try:
|
| 346 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=Path(original_filename).suffix) as tmp_file:
|
| 347 |
+
tmp_file.write(audio_bytes)
|
| 348 |
+
tmp_file_path = tmp_file.name
|
| 349 |
+
|
| 350 |
+
processor = AUDIO_PROCESSOR_CLASS()
|
| 351 |
+
result_data = None
|
| 352 |
+
full_text = ""
|
| 353 |
+
word_timestamps = []
|
| 354 |
+
|
| 355 |
+
# Determine which processing path to take
|
| 356 |
+
if st.session_state.enable_translation:
|
| 357 |
+
with st.spinner("⏳ Performing AI Transcription & Translation... please wait."):
|
| 358 |
+
result_data, processor_logs = processor.get_word_timestamps_with_translation(
|
| 359 |
+
tmp_file_path,
|
| 360 |
+
st.session_state.target_language,
|
| 361 |
+
)
|
| 362 |
+
|
| 363 |
+
log_to_browser_console(processor_logs)
|
| 364 |
+
|
| 365 |
+
if not result_data or not result_data.get("original_text"):
|
| 366 |
+
st.warning(
|
| 367 |
+
"Could not generate transcription with translation. Check browser console (F12) for logs."
|
| 368 |
+
)
|
| 369 |
+
return
|
| 370 |
+
|
| 371 |
+
st.session_state.transcription_data = {
|
| 372 |
+
"text": result_data["original_text"],
|
| 373 |
+
"translated_text": result_data["translated_text"],
|
| 374 |
+
"word_timestamps": result_data["word_timestamps"],
|
| 375 |
+
"audio_bytes": audio_bytes,
|
| 376 |
+
"original_suffix": Path(original_filename).suffix,
|
| 377 |
+
"translation_success": result_data.get("translation_success", False),
|
| 378 |
+
"detected_language": result_data.get("language_detected", "unknown"),
|
| 379 |
+
}
|
| 380 |
+
# Update transcript feed (prepend, dedupe by digest)
|
| 381 |
+
try:
|
| 382 |
+
digest = hashlib.md5(audio_bytes).hexdigest()
|
| 383 |
+
except Exception:
|
| 384 |
+
digest = f"snap-{int(time.time()*1000)}"
|
| 385 |
+
if digest not in st.session_state.transcript_ids:
|
| 386 |
+
st.session_state.transcript_ids.add(digest)
|
| 387 |
+
st.session_state.transcript_feed.insert(
|
| 388 |
+
0,
|
| 389 |
+
{
|
| 390 |
+
"id": digest,
|
| 391 |
+
"ts": int(time.time() * 1000),
|
| 392 |
+
"text": result_data["original_text"],
|
| 393 |
+
},
|
| 394 |
+
)
|
| 395 |
+
# Rebuild edited_text with newest first
|
| 396 |
+
st.session_state.edited_text = "\n\n".join(
|
| 397 |
+
[s["text"] for s in st.session_state.transcript_feed]
|
| 398 |
+
)
|
| 399 |
+
|
| 400 |
+
else: # Standard processing without translation
|
| 401 |
+
with st.spinner("⏳ Performing AI Transcription... please wait."):
|
| 402 |
+
word_timestamps, processor_logs = processor.get_word_timestamps(
|
| 403 |
+
tmp_file_path
|
| 404 |
+
)
|
| 405 |
+
|
| 406 |
+
log_to_browser_console(processor_logs)
|
| 407 |
+
|
| 408 |
+
if not word_timestamps:
|
| 409 |
+
st.warning(
|
| 410 |
+
"Could not generate timestamps. Check browser console (F12) for logs."
|
| 411 |
+
)
|
| 412 |
+
return
|
| 413 |
+
|
| 414 |
+
full_text = " ".join([d["word"] for d in word_timestamps])
|
| 415 |
+
st.session_state.transcription_data = {
|
| 416 |
+
"text": full_text,
|
| 417 |
+
"word_timestamps": word_timestamps,
|
| 418 |
+
"audio_bytes": audio_bytes,
|
| 419 |
+
"original_suffix": Path(original_filename).suffix,
|
| 420 |
+
"translation_success": False,
|
| 421 |
+
}
|
| 422 |
+
# Update transcript feed (prepend, dedupe by digest)
|
| 423 |
+
try:
|
| 424 |
+
digest = hashlib.md5(audio_bytes).hexdigest()
|
| 425 |
+
except Exception:
|
| 426 |
+
digest = f"snap-{int(time.time()*1000)}"
|
| 427 |
+
if digest not in st.session_state.transcript_ids:
|
| 428 |
+
st.session_state.transcript_ids.add(digest)
|
| 429 |
+
st.session_state.transcript_feed.insert(
|
| 430 |
+
0, {"id": digest, "ts": int(time.time() * 1000), "text": full_text}
|
| 431 |
+
)
|
| 432 |
+
# Rebuild edited_text with newest first
|
| 433 |
+
st.session_state.edited_text = "\n\n".join(
|
| 434 |
+
[s["text"] for s in st.session_state.transcript_feed]
|
| 435 |
+
)
|
| 436 |
+
|
| 437 |
+
st.session_state.step = 1 # Keep it on the same step
|
| 438 |
+
|
| 439 |
+
# Return result for background processing
|
| 440 |
+
return {
|
| 441 |
+
'original_text': result_data.get("original_text") if result_data else full_text,
|
| 442 |
+
'translated_text': result_data.get("translated_text") if result_data else None,
|
| 443 |
+
'detected_language': result_data.get("language_detected") if result_data else "unknown",
|
| 444 |
+
'translation_success': result_data.get("translation_success", False) if result_data else False,
|
| 445 |
+
'word_timestamps': result_data.get("word_timestamps") if result_data else word_timestamps
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
except Exception as e:
|
| 449 |
+
st.error("An unexpected error occurred during audio processing!")
|
| 450 |
+
st.exception(e)
|
| 451 |
+
log_to_browser_console(f"--- FATAL ERROR in run_audio_processing: {traceback.format_exc()} ---")
|
| 452 |
+
return None
|
| 453 |
+
finally:
|
| 454 |
+
if tmp_file_path and os.path.exists(tmp_file_path):
|
| 455 |
+
os.unlink(tmp_file_path)
|
| 456 |
+
|
| 457 |
+
|
| 458 |
+
# --- Main Application Logic ---
|
| 459 |
+
def main():
|
| 460 |
+
# Apply custom styling first
|
| 461 |
+
apply_custom_styling()
|
| 462 |
+
|
| 463 |
+
# Force reload environment variables
|
| 464 |
+
load_dotenv(override=True)
|
| 465 |
+
|
| 466 |
+
# Clear AI models cache on first run or if there's an issue
|
| 467 |
+
if 'app_initialized' not in st.session_state:
|
| 468 |
+
reset_ai_models()
|
| 469 |
+
st.session_state.app_initialized = True
|
| 470 |
+
|
| 471 |
+
initialize_session_state()
|
| 472 |
+
|
| 473 |
+
st.markdown("""
|
| 474 |
+
<style>
|
| 475 |
+
.main .block-container { animation: fadeIn 0.2s ease-in-out; }
|
| 476 |
+
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
| 477 |
+
.block-container { padding-top: 1rem; }
|
| 478 |
+
</style>
|
| 479 |
+
""", unsafe_allow_html=True)
|
| 480 |
+
|
| 481 |
+
with st.sidebar:
|
| 482 |
+
st.markdown("## 🌐 Language Settings")
|
| 483 |
+
language_options = {'English': 'en', 'العربية': 'ar'}
|
| 484 |
+
selected_lang_display = st.selectbox(
|
| 485 |
+
"Interface Language",
|
| 486 |
+
options=list(language_options.keys()),
|
| 487 |
+
index=0 if st.session_state.language == 'en' else 1
|
| 488 |
+
)
|
| 489 |
+
st.session_state.language = language_options[selected_lang_display]
|
| 490 |
+
|
| 491 |
+
st.markdown("## 🔤 Translation Settings")
|
| 492 |
+
st.session_state.enable_translation = st.checkbox(
|
| 493 |
+
"Enable AI Translation" if st.session_state.language == 'en' else "تفعيل الترجمة بالذكاء الاصطناعي",
|
| 494 |
+
value=st.session_state.enable_translation,
|
| 495 |
+
help="Automatically translate transcribed text" if st.session_state.language == 'en' else "ترجمة النص تلقائياً"
|
| 496 |
+
)
|
| 497 |
+
|
| 498 |
+
if st.session_state.enable_translation:
|
| 499 |
+
target_lang_options = {
|
| 500 |
+
'Arabic (العربية)': 'ar', 'English': 'en', 'French (Français)': 'fr', 'Spanish (Español)': 'es'
|
| 501 |
+
}
|
| 502 |
+
selected_target = st.selectbox(
|
| 503 |
+
"Target Language" if st.session_state.language == 'en' else "اللغة المستهدفة",
|
| 504 |
+
options=list(target_lang_options.keys()), index=0
|
| 505 |
+
)
|
| 506 |
+
st.session_state.target_language = target_lang_options[selected_target]
|
| 507 |
+
# Auto summary toggle
|
| 508 |
+
st.session_state.auto_generate_summary = st.checkbox(
|
| 509 |
+
"Auto-generate Arabic summary" if st.session_state.language == 'en' else "توليد الملخص العربي تلقائياً",
|
| 510 |
+
value=st.session_state.auto_generate_summary
|
| 511 |
+
)
|
| 512 |
+
|
| 513 |
+
# Google Account Status
|
| 514 |
+
st.markdown("## 🔐 Google Account")
|
| 515 |
+
if google_docs_manager.is_authenticated():
|
| 516 |
+
st.success("✅ متصل" if st.session_state.language == 'ar' else "✅ Connected")
|
| 517 |
+
else:
|
| 518 |
+
st.info("🔒 غير متصل" if st.session_state.language == 'ar' else "🔒 Not connected")
|
| 519 |
+
|
| 520 |
+
# AI Questions Status
|
| 521 |
+
st.markdown("## 🤖 AI Questions")
|
| 522 |
+
|
| 523 |
+
# Show preferred model
|
| 524 |
+
if st.session_state.preferred_ai_model != 'auto':
|
| 525 |
+
st.info(f"🎯 {'النموذج المفضل' if st.session_state.language == 'ar' else 'Preferred Model'}: {st.session_state.preferred_ai_model}")
|
| 526 |
+
else:
|
| 527 |
+
st.info("🔄 " + ("تلقائي" if st.session_state.language == 'ar' else "Auto selection"))
|
| 528 |
+
|
| 529 |
+
# Model reset button
|
| 530 |
+
if st.button("🔄 " + ("إعادة تعيين النماذج" if st.session_state.language == 'ar' else "Reset AI Models"), help="إعادة تحميل نماذج الذكاء الاصطناعي" if st.session_state.language == 'ar' else "Reload AI models"):
|
| 531 |
+
reset_ai_models()
|
| 532 |
+
st.success("✅ " + ("تم إعادة تعيين النماذج" if st.session_state.language == 'ar' else "AI models reset successfully"))
|
| 533 |
+
st.rerun()
|
| 534 |
+
|
| 535 |
+
# Show preferred answer language
|
| 536 |
+
answer_lang = st.session_state.get('preferred_answer_language', 'auto')
|
| 537 |
+
if answer_lang != 'auto':
|
| 538 |
+
lang_names = {'ar': '🇸🇦 العربية', 'en': '🇺🇸 English', 'fr': '🇫🇷 Français', 'es': '🇪🇸 Español', 'de': '🇩🇪 Deutsch', 'zh': '🇨🇳 中文'}
|
| 539 |
+
lang_display = lang_names.get(answer_lang, answer_lang)
|
| 540 |
+
st.info(f"🌐 {'لغة الإجابة' if st.session_state.language == 'ar' else 'Answer Language'}: {lang_display}")
|
| 541 |
+
else:
|
| 542 |
+
current_ui_lang = "🇸🇦 العربية" if st.session_state.language == 'ar' else "🇺🇸 English"
|
| 543 |
+
st.info(f"🌐 {'لغة الإجابة' if st.session_state.language == 'ar' else 'Answer Language'}: {current_ui_lang} ({'تلقائي' if st.session_state.language == 'ar' else 'Auto'})")
|
| 544 |
+
|
| 545 |
+
# Test AI services availability
|
| 546 |
+
translator = get_translator()
|
| 547 |
+
services_status = {}
|
| 548 |
+
|
| 549 |
+
# Test Gemini
|
| 550 |
+
try:
|
| 551 |
+
if hasattr(translator, 'model') and translator.model:
|
| 552 |
+
test_response = translator.model.generate_content("Test")
|
| 553 |
+
services_status['Gemini'] = "✅"
|
| 554 |
+
else:
|
| 555 |
+
services_status['Gemini'] = "❌"
|
| 556 |
+
except Exception as e:
|
| 557 |
+
error_str = str(e)
|
| 558 |
+
if "429" in error_str or "quota" in error_str.lower():
|
| 559 |
+
services_status['Gemini'] = "⚠️"
|
| 560 |
+
else:
|
| 561 |
+
services_status['Gemini'] = "❌"
|
| 562 |
+
|
| 563 |
+
# Test Groq
|
| 564 |
+
try:
|
| 565 |
+
if hasattr(translator, '_groq_complete') and translator.groq_api_key:
|
| 566 |
+
services_status['Groq'] = "✅"
|
| 567 |
+
else:
|
| 568 |
+
services_status['Groq'] = "❌"
|
| 569 |
+
except Exception:
|
| 570 |
+
services_status['Groq'] = "❌"
|
| 571 |
+
|
| 572 |
+
# Test OpenRouter
|
| 573 |
+
try:
|
| 574 |
+
if hasattr(translator, '_openrouter_complete') and translator.openrouter_api_key:
|
| 575 |
+
services_status['OpenRouter'] = "✅"
|
| 576 |
+
else:
|
| 577 |
+
services_status['OpenRouter'] = "❌"
|
| 578 |
+
except Exception:
|
| 579 |
+
services_status['OpenRouter'] = "❌"
|
| 580 |
+
|
| 581 |
+
# Display services status
|
| 582 |
+
st.markdown("**" + ("حالة النماذج" if st.session_state.language == 'ar' else "Models Status") + ":**")
|
| 583 |
+
for service, status in services_status.items():
|
| 584 |
+
if status == "✅":
|
| 585 |
+
st.success(f"{status} {service}")
|
| 586 |
+
elif status == "⚠️":
|
| 587 |
+
st.warning(f"{status} {service} (حد يومي)" if st.session_state.language == 'ar' else f"{status} {service} (quota)")
|
| 588 |
+
else:
|
| 589 |
+
st.error(f"{status} {service}")
|
| 590 |
+
|
| 591 |
+
# Overall status
|
| 592 |
+
available_count = sum(1 for status in services_status.values() if status == "✅")
|
| 593 |
+
if available_count > 0:
|
| 594 |
+
st.info(f"🤖 {available_count}/3 " + ("نماذج متاحة" if st.session_state.language == 'ar' else "models available"))
|
| 595 |
+
else:
|
| 596 |
+
st.warning("⚠️ جميع النماذج غير متاحة" if st.session_state.language == 'ar' else "⚠️ All models unavailable")
|
| 597 |
+
|
| 598 |
+
# Show conversation status and usage stats
|
| 599 |
+
question_engine = get_ai_question_engine()
|
| 600 |
+
usage_stats = question_engine.get_model_usage_stats()
|
| 601 |
+
total_questions = sum(usage_stats.values())
|
| 602 |
+
|
| 603 |
+
if st.session_state.current_question_session:
|
| 604 |
+
session = question_engine.get_conversation_history(st.session_state.current_question_session)
|
| 605 |
+
if session and session.conversation:
|
| 606 |
+
st.info(f"💬 {len(session.conversation)} " + ("أسئلة نشطة" if st.session_state.language == 'ar' else "active questions"))
|
| 607 |
+
else:
|
| 608 |
+
st.info("🤖 جاهز للأسئلة" if st.session_state.language == 'ar' else "🤖 Ready for questions")
|
| 609 |
+
else:
|
| 610 |
+
st.info("🤖 جاهز للأسئلة" if st.session_state.language == 'ar' else "🤖 Ready for questions")
|
| 611 |
+
|
| 612 |
+
# Show usage statistics
|
| 613 |
+
if total_questions > 0:
|
| 614 |
+
with st.expander("📊 إحصائيات الاستخدام" if st.session_state.language == 'ar' else "📊 Usage Statistics"):
|
| 615 |
+
st.write(f"**{'إجمالي الأسئلة' if st.session_state.language == 'ar' else 'Total Questions'}: {total_questions}**")
|
| 616 |
+
for model, count in usage_stats.items():
|
| 617 |
+
if count > 0:
|
| 618 |
+
percentage = (count / total_questions) * 100
|
| 619 |
+
st.write(f"• {model}: {count} ({percentage:.1f}%)")
|
| 620 |
+
else:
|
| 621 |
+
st.caption("📊 لا توجد إحصائيات بعد" if st.session_state.language == 'ar' else "📊 No statistics yet")
|
| 622 |
+
|
| 623 |
+
# Quick model switcher
|
| 624 |
+
st.markdown("**" + ("تبديل سريع للنموذج" if st.session_state.language == 'ar' else "Quick Model Switch") + "**")
|
| 625 |
+
|
| 626 |
+
question_engine = get_ai_question_engine()
|
| 627 |
+
models_status = question_engine.check_model_availability()
|
| 628 |
+
|
| 629 |
+
# Create buttons for each available model
|
| 630 |
+
cols = st.columns(2)
|
| 631 |
+
model_names = ['auto', 'Gemini AI', 'Groq AI', 'OpenRouter AI']
|
| 632 |
+
|
| 633 |
+
for i, model in enumerate(model_names):
|
| 634 |
+
with cols[i % 2]:
|
| 635 |
+
if model == 'auto':
|
| 636 |
+
button_text = "🔄 تلقائي" if st.session_state.language == 'ar' else "🔄 Auto"
|
| 637 |
+
is_current = st.session_state.preferred_ai_model == 'auto'
|
| 638 |
+
else:
|
| 639 |
+
status_info = models_status.get(model, {})
|
| 640 |
+
icon = status_info.get('icon', '❓')
|
| 641 |
+
button_text = f"{icon} {model.split()[0]}" # Show first word + icon
|
| 642 |
+
is_current = st.session_state.preferred_ai_model == model
|
| 643 |
+
|
| 644 |
+
button_type = "primary" if is_current else "secondary"
|
| 645 |
+
|
| 646 |
+
if st.button(button_text, key=f"switch_{model}", type=button_type, use_container_width=True):
|
| 647 |
+
st.session_state.preferred_ai_model = model
|
| 648 |
+
st.rerun()
|
| 649 |
+
|
| 650 |
+
# Quick language switcher
|
| 651 |
+
st.markdown("**" + ("تبديل سريع للغة" if st.session_state.language == 'ar' else "Quick Language Switch") + "**")
|
| 652 |
+
|
| 653 |
+
language_buttons = {
|
| 654 |
+
'auto': "🔄 تلقائي" if st.session_state.language == 'ar' else "🔄 Auto",
|
| 655 |
+
'ar': "🇸🇦 عربي",
|
| 656 |
+
'en': "🇺🇸 EN",
|
| 657 |
+
'fr': "🇫🇷 FR",
|
| 658 |
+
'es': "🇪🇸 ES"
|
| 659 |
+
}
|
| 660 |
+
|
| 661 |
+
cols_lang = st.columns(3)
|
| 662 |
+
for i, (lang_code, button_text) in enumerate(language_buttons.items()):
|
| 663 |
+
with cols_lang[i % 3]:
|
| 664 |
+
is_current_lang = st.session_state.get('preferred_answer_language', 'auto') == lang_code
|
| 665 |
+
button_type_lang = "primary" if is_current_lang else "secondary"
|
| 666 |
+
|
| 667 |
+
if st.button(button_text, key=f"switch_lang_{lang_code}", type=button_type_lang, use_container_width=True):
|
| 668 |
+
st.session_state.preferred_answer_language = lang_code
|
| 669 |
+
st.rerun()
|
| 670 |
+
|
| 671 |
+
# Processing status
|
| 672 |
+
if st.session_state.processing_queue or st.session_state.processing_results:
|
| 673 |
+
st.markdown("## 🔄 " + ("حالة المعالجة" if st.session_state.language == 'ar' else "Processing Status"))
|
| 674 |
+
|
| 675 |
+
# Queue status
|
| 676 |
+
if st.session_state.processing_queue:
|
| 677 |
+
queue_count = len(st.session_state.processing_queue)
|
| 678 |
+
st.warning(f"⏳ {queue_count} " + ("في الانتظار" if st.session_state.language == 'ar' else "in queue"))
|
| 679 |
+
|
| 680 |
+
# Results count
|
| 681 |
+
if st.session_state.processing_results:
|
| 682 |
+
results_count = len(st.session_state.processing_results)
|
| 683 |
+
st.success(f"✅ {results_count} " + ("مكتمل" if st.session_state.language == 'ar' else "completed"))
|
| 684 |
+
|
| 685 |
+
# Clear all button
|
| 686 |
+
if st.button("🗑️ " + ("مسح الكل" if st.session_state.language == 'ar' else "Clear All")):
|
| 687 |
+
st.session_state.processing_queue = []
|
| 688 |
+
st.session_state.processing_results = {}
|
| 689 |
+
st.session_state.processing_status = {}
|
| 690 |
+
st.rerun()
|
| 691 |
+
|
| 692 |
+
st.title("🎵 SyncMaster")
|
| 693 |
+
if st.session_state.language == 'ar':
|
| 694 |
+
st.markdown("### منصة المزامنة الذكية بين الصوت والنص")
|
| 695 |
+
else:
|
| 696 |
+
st.markdown("### The Intelligent Audio-Text Synchronization Platform")
|
| 697 |
+
|
| 698 |
+
# Simplified interface - removed step indicators as requested
|
| 699 |
+
# Global settings for long recording retention and custom snapshot duration
|
| 700 |
+
with st.expander("⚙️ Recording Settings (Snapshots)", expanded=False):
|
| 701 |
+
st.session_state.setdefault('retention_minutes', 30)
|
| 702 |
+
# 0 means: use full buffer by default for Custom
|
| 703 |
+
st.session_state.setdefault('custom_snapshot_seconds', 0)
|
| 704 |
+
# Auto-Custom interval seconds (for frontend auto trigger)
|
| 705 |
+
st.session_state.setdefault('auto_custom_interval_sec', 10)
|
| 706 |
+
# Auto-start incremental snapshots when recording begins
|
| 707 |
+
st.session_state.setdefault('auto_start_custom', False)
|
| 708 |
+
st.session_state.retention_minutes = st.number_input("Retention window (minutes)", min_value=5, max_value=240, value=st.session_state.retention_minutes)
|
| 709 |
+
st.session_state.custom_snapshot_seconds = st.number_input("Custom snapshot (seconds; 0 = full buffer)", min_value=0, max_value=3600, value=st.session_state.custom_snapshot_seconds)
|
| 710 |
+
st.session_state.auto_custom_interval_sec = st.number_input("Auto Custom interval (seconds)", min_value=1, max_value=3600, value=st.session_state.auto_custom_interval_sec, help="How often to auto-trigger the same Custom action while recording.")
|
| 711 |
+
st.session_state.auto_start_custom = st.checkbox("Auto-start incremental snapshots on record", value=st.session_state.auto_start_custom, help="Start sending Custom intervals automatically as soon as you start recording.")
|
| 712 |
+
# Inject globals into the page for the component to pick up
|
| 713 |
+
components.html(f"""
|
| 714 |
+
<script>
|
| 715 |
+
window.ST_AREC_RETENTION_MINUTES = {int(st.session_state.retention_minutes)};
|
| 716 |
+
window.ST_AREC_CUSTOM_SNAPSHOT_SECONDS = {int(st.session_state.custom_snapshot_seconds)};
|
| 717 |
+
window.ST_AREC_LAST_FETCHED_END_MS = {int(st.session_state.get('lastFetchedEnd_ms', 0))};
|
| 718 |
+
window.ST_AREC_CUSTOM_AUTO_INTERVAL_SECONDS = {int(st.session_state.get('auto_custom_interval_sec', 10))};
|
| 719 |
+
window.ST_AREC_AUTO_START = {str(bool(st.session_state.get('auto_start_custom', True))).lower()};
|
| 720 |
+
console.log('Recorder config', window.ST_AREC_RETENTION_MINUTES, window.ST_AREC_CUSTOM_SNAPSHOT_SECONDS);
|
| 721 |
+
</script>
|
| 722 |
+
""", height=0)
|
| 723 |
+
|
| 724 |
+
if AUDIO_PROCESSOR_CLASS is None:
|
| 725 |
+
st.error("Fatal Error: The application could not start correctly.")
|
| 726 |
+
st.subheader("An error occurred while trying to import `AudioProcessor`:")
|
| 727 |
+
st.code(IMPORT_ERROR_TRACEBACK, language="python")
|
| 728 |
+
st.stop()
|
| 729 |
+
|
| 730 |
+
step_1_upload_and_process()
|
| 731 |
+
|
| 732 |
+
# Process background queue
|
| 733 |
+
if st.session_state.get('background_processing', True) and st.session_state.processing_queue:
|
| 734 |
+
process_queued_audio()
|
| 735 |
+
|
| 736 |
+
# Show processing results optionally
|
| 737 |
+
if st.session_state.get('show_processing_results', False):
|
| 738 |
+
show_processing_results()
|
| 739 |
+
elif st.session_state.processing_results:
|
| 740 |
+
# Show a button to view results if there are any
|
| 741 |
+
if st.button("📝 " + ("عرض نتائج المعالجة" if st.session_state.language == 'ar' else "Show Processing Results") + f" ({len(st.session_state.processing_results)})", type="secondary"):
|
| 742 |
+
st.session_state.show_processing_results = True
|
| 743 |
+
st.rerun()
|
| 744 |
+
|
| 745 |
+
# Note: step_2_review_and_customize removed as requested
|
| 746 |
+
# Results are now shown in show_processing_results() function
|
| 747 |
+
|
| 748 |
+
# AI Question modal (show outside of other components)
|
| 749 |
+
if st.session_state.show_question_modal:
|
| 750 |
+
show_question_modal()
|
| 751 |
+
|
| 752 |
+
# Export modal (show outside of other components)
|
| 753 |
+
if st.session_state.show_export_modal:
|
| 754 |
+
show_export_modal()
|
| 755 |
+
|
| 756 |
+
# --- Show Processing Results ---
|
| 757 |
+
def show_processing_results():
|
| 758 |
+
"""Show processing results in the same page"""
|
| 759 |
+
|
| 760 |
+
if not st.session_state.processing_results:
|
| 761 |
+
return
|
| 762 |
+
|
| 763 |
+
st.markdown("---")
|
| 764 |
+
|
| 765 |
+
# Header with results count and close button
|
| 766 |
+
col_header, col_close = st.columns([4, 1])
|
| 767 |
+
|
| 768 |
+
with col_header:
|
| 769 |
+
results_count = len(st.session_state.processing_results)
|
| 770 |
+
st.subheader(f"📝 {'نتائج المعالجة' if st.session_state.language == 'ar' else 'Processing Results'} ({results_count})")
|
| 771 |
+
|
| 772 |
+
with col_close:
|
| 773 |
+
if st.button("❌ " + ("إخفاء" if st.session_state.language == 'ar' else "Hide"), key="hide_results"):
|
| 774 |
+
st.session_state.show_processing_results = False
|
| 775 |
+
st.rerun()
|
| 776 |
+
|
| 777 |
+
if results_count > 1:
|
| 778 |
+
# Show all results in one view option
|
| 779 |
+
show_all = st.checkbox(
|
| 780 |
+
"عرض جميع النتائج مجمعة" if st.session_state.language == 'ar' else "Show all results combined",
|
| 781 |
+
help="عرض جميع النصوص والترجمات في مكان واحد" if st.session_state.language == 'ar' else "Display all texts and translations in one place"
|
| 782 |
+
)
|
| 783 |
+
|
| 784 |
+
if show_all:
|
| 785 |
+
# Combined view
|
| 786 |
+
st.markdown("### " + ("النصوص الأصلية مجمعة" if st.session_state.language == 'ar' else "Combined Original Texts"))
|
| 787 |
+
combined_original = "\n\n".join([result.get('original_text', '') for result in st.session_state.processing_results.values() if result.get('original_text')])
|
| 788 |
+
if combined_original:
|
| 789 |
+
st.write(combined_original)
|
| 790 |
+
|
| 791 |
+
if st.button("📋 " + ("نسخ جميع النصوص" if st.session_state.language == 'ar' else "Copy All Texts")):
|
| 792 |
+
st.code(combined_original, language=None)
|
| 793 |
+
|
| 794 |
+
st.markdown("### " + ("الترجمات مجمعة" if st.session_state.language == 'ar' else "Combined Translations"))
|
| 795 |
+
combined_translation = "\n\n".join([result.get('translated_text', '') for result in st.session_state.processing_results.values() if result.get('translated_text')])
|
| 796 |
+
if combined_translation:
|
| 797 |
+
st.write(combined_translation)
|
| 798 |
+
|
| 799 |
+
if st.button("📋 " + ("نسخ جميع الترجمات" if st.session_state.language == 'ar' else "Copy All Translations")):
|
| 800 |
+
st.code(combined_translation, language=None)
|
| 801 |
+
|
| 802 |
+
st.markdown("---")
|
| 803 |
+
|
| 804 |
+
# Show results for each completed processing
|
| 805 |
+
for task_id, result in st.session_state.processing_results.items():
|
| 806 |
+
with st.expander(f"🎵 {'التسجيل' if st.session_state.language == 'ar' else 'Recording'} {task_id}", expanded=True):
|
| 807 |
+
|
| 808 |
+
# Original text in white container
|
| 809 |
+
if result.get('original_text'):
|
| 810 |
+
original_title = "النص الأصلي" if st.session_state.language == 'ar' else "Original Text"
|
| 811 |
+
original_container = create_white_container(original_title, result['original_text'], "📝")
|
| 812 |
+
st.markdown(original_container, unsafe_allow_html=True)
|
| 813 |
+
|
| 814 |
+
# Translation in white container
|
| 815 |
+
if result.get('translated_text'):
|
| 816 |
+
translation_title = "الترجمة" if st.session_state.language == 'ar' else "Translation"
|
| 817 |
+
translation_container = create_white_container(translation_title, result['translated_text'], "🌐")
|
| 818 |
+
st.markdown(translation_container, unsafe_allow_html=True)
|
| 819 |
+
|
| 820 |
+
# Language info
|
| 821 |
+
if result.get('detected_language'):
|
| 822 |
+
st.caption(f"🌐 {'اللغة المكتشفة' if st.session_state.language == 'ar' else 'Detected language'}: {result['detected_language']}")
|
| 823 |
+
|
| 824 |
+
# Action buttons
|
| 825 |
+
col1, col2, col3 = st.columns(3)
|
| 826 |
+
|
| 827 |
+
with col1:
|
| 828 |
+
if st.button(f"📋 {'نسخ النص' if st.session_state.language == 'ar' else 'Copy Text'}", key=f"copy_original_{task_id}"):
|
| 829 |
+
st.code(result.get('original_text', ''), language=None)
|
| 830 |
+
st.success("✅ " + ("تم تنسيق النص للنسخ" if st.session_state.language == 'ar' else "Text formatted for copying"))
|
| 831 |
+
|
| 832 |
+
with col2:
|
| 833 |
+
if result.get('translated_text') and st.button(f"📋 {'نسخ الترجمة' if st.session_state.language == 'ar' else 'Copy Translation'}", key=f"copy_translation_{task_id}"):
|
| 834 |
+
st.code(result.get('translated_text', ''), language=None)
|
| 835 |
+
st.success("✅ " + ("تم تنسيق الترجمة للنسخ" if st.session_state.language == 'ar' else "Translation formatted for copying"))
|
| 836 |
+
|
| 837 |
+
with col3:
|
| 838 |
+
if st.button(f"🗑️ {'حذف' if st.session_state.language == 'ar' else 'Delete'}", key=f"delete_{task_id}"):
|
| 839 |
+
del st.session_state.processing_results[task_id]
|
| 840 |
+
if task_id in st.session_state.processing_status:
|
| 841 |
+
del st.session_state.processing_status[task_id]
|
| 842 |
+
st.rerun()
|
| 843 |
+
|
| 844 |
+
# --- Step 1: Upload and Process ---
|
| 845 |
+
def step_1_upload_and_process():
|
| 846 |
+
st.header("🎵 " + ("مصدر الصوت" if st.session_state.language == 'ar' else "Audio Source"))
|
| 847 |
+
|
| 848 |
+
upload_tab, record_tab = st.tabs(["📤 Upload a File", "🎙️ Record Audio"])
|
| 849 |
+
|
| 850 |
+
with upload_tab:
|
| 851 |
+
st.subheader("Upload an existing audio file")
|
| 852 |
+
uploaded_file = st.file_uploader("Choose an audio file", type=['mp3', 'wav', 'm4a'], help="Supported formats: MP3, WAV, M4A")
|
| 853 |
+
if uploaded_file:
|
| 854 |
+
st.session_state.audio_data = uploaded_file.getvalue()
|
| 855 |
+
st.success(f"File ready for processing: {uploaded_file.name}")
|
| 856 |
+
st.audio(st.session_state.audio_data)
|
| 857 |
+
if st.button("🚀 Start AI Processing", type="primary", use_container_width=True):
|
| 858 |
+
run_audio_processing(st.session_state.audio_data, uploaded_file.name)
|
| 859 |
+
if st.session_state.audio_data:
|
| 860 |
+
if st.button("🔄 Use a Different File"):
|
| 861 |
+
reset_session()
|
| 862 |
+
st.rerun()
|
| 863 |
+
|
| 864 |
+
with record_tab:
|
| 865 |
+
st.subheader("Record audio directly from your microphone")
|
| 866 |
+
|
| 867 |
+
# Recording instructions
|
| 868 |
+
# Recording instructions with improved controls
|
| 869 |
+
if st.session_state.language == 'ar':
|
| 870 |
+
st.info("🎙️ **تحكم بسيط في التسجيل:**\n- اضغط الميكروفون لبدء التسجيل\n- اضغط مرة أخرى للتوقف\n- استخدم الأزرار أدناه للتحكم الإضافي")
|
| 871 |
+
else:
|
| 872 |
+
st.info("🎙️ **Simple Recording Controls:**\n- Click microphone to start recording\n- Click again to stop\n- Use buttons below for additional control")
|
| 873 |
+
|
| 874 |
+
# Recording control buttons
|
| 875 |
+
col_record_info, col_record_controls = st.columns([2, 1])
|
| 876 |
+
|
| 877 |
+
with col_record_info:
|
| 878 |
+
# This will show recording status
|
| 879 |
+
pass
|
| 880 |
+
|
| 881 |
+
with col_record_controls:
|
| 882 |
+
# Recording control buttons in a more compact layout
|
| 883 |
+
col_pause, col_resume = st.columns(2)
|
| 884 |
+
|
| 885 |
+
with col_pause:
|
| 886 |
+
if st.button("⏸️ " + ("إيقاف مؤقت" if st.session_state.language == 'ar' else "Pause"),
|
| 887 |
+
help="إيقاف مؤقت للتسجيل" if st.session_state.language == 'ar' else "Pause recording",
|
| 888 |
+
use_container_width=True):
|
| 889 |
+
st.info("💡 " + ("استخدم زر الميكروفون للإيقاف المؤقت" if st.session_state.language == 'ar' else "Use microphone button to pause"))
|
| 890 |
+
|
| 891 |
+
with col_resume:
|
| 892 |
+
if st.button("▶️ " + ("استئناف" if st.session_state.language == 'ar' else "Resume"),
|
| 893 |
+
help="استئناف التسجيل" if st.session_state.language == 'ar' else "Resume recording",
|
| 894 |
+
use_container_width=True):
|
| 895 |
+
st.info("💡 " + ("استخدم زر الميكروفون للاستئناف" if st.session_state.language == 'ar' else "Use microphone button to resume"))
|
| 896 |
+
|
| 897 |
+
# Recording status
|
| 898 |
+
recording_status_placeholder = st.empty()
|
| 899 |
+
|
| 900 |
+
# Use the audio recorder component
|
| 901 |
+
wav_audio_data = st_audiorec()
|
| 902 |
+
|
| 903 |
+
# Show recording status and controls
|
| 904 |
+
if wav_audio_data:
|
| 905 |
+
# Check if wav_audio_data is bytes or dict
|
| 906 |
+
if isinstance(wav_audio_data, bytes):
|
| 907 |
+
# Simple bytes data - show audio player
|
| 908 |
+
recording_status_placeholder.success("🎵 " + ("تسجيل جاهز للمعالجة" if st.session_state.language == 'ar' else "Recording ready for processing"))
|
| 909 |
+
|
| 910 |
+
# Recording controls
|
| 911 |
+
col_play, col_clear = st.columns(2)
|
| 912 |
+
|
| 913 |
+
with col_play:
|
| 914 |
+
st.audio(wav_audio_data, format='audio/wav')
|
| 915 |
+
|
| 916 |
+
with col_clear:
|
| 917 |
+
if st.button("🗑️ " + ("مسح التسجيل" if st.session_state.language == 'ar' else "Clear Recording")):
|
| 918 |
+
st.rerun()
|
| 919 |
+
|
| 920 |
+
elif isinstance(wav_audio_data, dict):
|
| 921 |
+
# Dict data - handle interval processing
|
| 922 |
+
recording_status_placeholder.info("🔄 " + ("معالجة المقاطع..." if st.session_state.language == 'ar' else "Processing intervals..."))
|
| 923 |
+
|
| 924 |
+
# Google Docs Export and Logout Buttons
|
| 925 |
+
col_export, col_logout = st.columns([3, 1])
|
| 926 |
+
|
| 927 |
+
with col_export:
|
| 928 |
+
export_button_text = "📤 تصدير إلى Google Docs" if st.session_state.language == 'ar' else "📤 Export to Google Docs"
|
| 929 |
+
if st.button(export_button_text, type="primary", use_container_width=True):
|
| 930 |
+
export_to_google_docs_directly()
|
| 931 |
+
|
| 932 |
+
with col_logout:
|
| 933 |
+
# Check if user is authenticated
|
| 934 |
+
if google_docs_manager.is_authenticated():
|
| 935 |
+
logout_text = "🚪 خروج" if st.session_state.language == 'ar' else "🚪 Logout"
|
| 936 |
+
if st.button(logout_text, use_container_width=True, help="تسجيل الخروج من Google" if st.session_state.language == 'ar' else "Logout from Google"):
|
| 937 |
+
logout_from_google()
|
| 938 |
+
else:
|
| 939 |
+
# Show login status
|
| 940 |
+
login_status = "غير متصل" if st.session_state.language == 'ar' else "Not logged in"
|
| 941 |
+
st.caption(f"🔒 {login_status}")
|
| 942 |
+
|
| 943 |
+
# Processing settings
|
| 944 |
+
st.markdown("**" + ("إعدادات المعالجة" if st.session_state.language == 'ar' else "Processing Settings") + "**")
|
| 945 |
+
|
| 946 |
+
# Auto-process toggle (changed default to False for better UX)
|
| 947 |
+
st.session_state.setdefault('auto_process_snapshots', False)
|
| 948 |
+
auto_process = st.checkbox(
|
| 949 |
+
"معالجة تلقائية للمقاطع" if st.session_state.language == 'ar' else "Auto-process snapshots",
|
| 950 |
+
key='auto_process_snapshots',
|
| 951 |
+
help="عند التفعيل، يتم معالجة المقاطع تلقائياً أثناء التسجيل" if st.session_state.language == 'ar' else "When enabled, snapshots are processed automatically during recording"
|
| 952 |
+
)
|
| 953 |
+
|
| 954 |
+
# Background processing toggle
|
| 955 |
+
st.session_state.setdefault('background_processing', True)
|
| 956 |
+
background_mode = st.checkbox(
|
| 957 |
+
"معالجة في الخلفية" if st.session_state.language == 'ar' else "Background processing",
|
| 958 |
+
key='background_processing',
|
| 959 |
+
value=True,
|
| 960 |
+
help="يسمح بالاستمرار في استخدام التطبيق أثناء المعالجة" if st.session_state.language == 'ar' else "Allows continued use of the app during processing"
|
| 961 |
+
)
|
| 962 |
+
|
| 963 |
+
if wav_audio_data:
|
| 964 |
+
# Two possible payload shapes: raw bytes array (legacy) or interval payload dict
|
| 965 |
+
if isinstance(wav_audio_data, dict) and wav_audio_data.get('type') in ('interval_wav', 'no_new'):
|
| 966 |
+
payload = wav_audio_data
|
| 967 |
+
# Mark Custom interval flow active so Step 2 editor/style can be hidden
|
| 968 |
+
st.session_state['_custom_active'] = True
|
| 969 |
+
if payload['type'] == 'no_new':
|
| 970 |
+
st.info("No new audio chunks yet.")
|
| 971 |
+
elif payload['type'] == 'interval_wav':
|
| 972 |
+
# Extract interval audio
|
| 973 |
+
b = bytes(payload['bytes'])
|
| 974 |
+
sr = int(payload.get('sr', 16000))
|
| 975 |
+
start_ms = int(payload['start_ms'])
|
| 976 |
+
end_ms = int(payload['end_ms'])
|
| 977 |
+
# Dedupe/trim logic
|
| 978 |
+
if end_ms <= start_ms:
|
| 979 |
+
st.warning("The received interval is empty.")
|
| 980 |
+
else:
|
| 981 |
+
# Prevent overlap with prior segment
|
| 982 |
+
last_end = st.session_state.lastFetchedEnd_ms or 0
|
| 983 |
+
eff_start_ms = max(start_ms, last_end)
|
| 984 |
+
if eff_start_ms < end_ms:
|
| 985 |
+
# If there is overlap, trim the audio bytes accordingly (assumes WAV PCM16 mono header 44 bytes)
|
| 986 |
+
try:
|
| 987 |
+
delta_ms = eff_start_ms - start_ms
|
| 988 |
+
if delta_ms > 0:
|
| 989 |
+
if len(b) >= 44 and b[0:4] == b'RIFF' and b[8:12] == b'WAVE':
|
| 990 |
+
bytes_per_sample = 2 # PCM16 mono
|
| 991 |
+
drop_samples = int(sr * (delta_ms / 1000.0))
|
| 992 |
+
drop_bytes = drop_samples * bytes_per_sample
|
| 993 |
+
data_size = int.from_bytes(b[40:44], 'little') if len(b) >= 44 else len(b) - 44
|
| 994 |
+
pcm = b[44:]
|
| 995 |
+
if drop_bytes < len(pcm):
|
| 996 |
+
pcm_trim = pcm[drop_bytes:]
|
| 997 |
+
else:
|
| 998 |
+
pcm_trim = b''
|
| 999 |
+
new_data_size = len(pcm_trim)
|
| 1000 |
+
# Rebuild header sizes
|
| 1001 |
+
header = bytearray(b[:44])
|
| 1002 |
+
# ChunkSize at offset 4 = 36 + Subchunk2Size
|
| 1003 |
+
(36 + new_data_size).to_bytes(4, 'little')
|
| 1004 |
+
header[4:8] = (36 + new_data_size).to_bytes(4, 'little')
|
| 1005 |
+
# Subchunk2Size at offset 40
|
| 1006 |
+
header[40:44] = new_data_size.to_bytes(4, 'little')
|
| 1007 |
+
b = bytes(header) + pcm_trim
|
| 1008 |
+
else:
|
| 1009 |
+
# Not a recognizable WAV header; keep as-is
|
| 1010 |
+
pass
|
| 1011 |
+
except Exception as _:
|
| 1012 |
+
pass
|
| 1013 |
+
# Compute checksum
|
| 1014 |
+
digest = hashlib.md5(b).hexdigest()
|
| 1015 |
+
# Skip if identical checksum and same window
|
| 1016 |
+
exists = any(s.get('checksum') == digest and s.get('start_ms') == eff_start_ms and s.get('end_ms') == end_ms for s in st.session_state.broadcast_segments)
|
| 1017 |
+
if not exists:
|
| 1018 |
+
# Show spinner during extraction so the user sees a waiting icon until text appears
|
| 1019 |
+
with st.spinner("⏳ Extracting text from interval..."):
|
| 1020 |
+
# Run standard pipeline to get text (no translation to keep it light)
|
| 1021 |
+
# Reuse run_audio_processing internals via a temp path
|
| 1022 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix='.wav') as tf:
|
| 1023 |
+
tf.write(b)
|
| 1024 |
+
tmp_path = tf.name
|
| 1025 |
+
try:
|
| 1026 |
+
processor = AUDIO_PROCESSOR_CLASS()
|
| 1027 |
+
word_timestamps, processor_logs, model_used = processor.get_word_timestamps(tmp_path)
|
| 1028 |
+
full_text = " ".join([d['word'] for d in word_timestamps]) if word_timestamps else ""
|
| 1029 |
+
# Fallback: if timestamps extraction yielded no words, try plain transcription
|
| 1030 |
+
if not full_text:
|
| 1031 |
+
plain_text, err, fallback_model = processor.transcribe_audio(tmp_path)
|
| 1032 |
+
if plain_text:
|
| 1033 |
+
full_text = plain_text.strip()
|
| 1034 |
+
model_used = fallback_model
|
| 1035 |
+
finally:
|
| 1036 |
+
if os.path.exists(tmp_path): os.unlink(tmp_path)
|
| 1037 |
+
|
| 1038 |
+
# Append segment immediately with only the original text
|
| 1039 |
+
seg = {
|
| 1040 |
+
'id': digest,
|
| 1041 |
+
'recording_id': payload.get('session_id', 'local'),
|
| 1042 |
+
'start_ms': eff_start_ms,
|
| 1043 |
+
'end_ms': end_ms,
|
| 1044 |
+
'checksum': digest,
|
| 1045 |
+
'text': full_text,
|
| 1046 |
+
'translations': {},
|
| 1047 |
+
'transcription_model': model_used,
|
| 1048 |
+
}
|
| 1049 |
+
st.session_state.broadcast_segments.append(seg)
|
| 1050 |
+
st.session_state.broadcast_segments.sort(key=lambda s: s['start_ms'])
|
| 1051 |
+
st.session_state.lastFetchedEnd_ms = end_ms
|
| 1052 |
+
if full_text:
|
| 1053 |
+
if digest not in st.session_state.transcript_ids:
|
| 1054 |
+
st.session_state.transcript_ids.add(digest)
|
| 1055 |
+
st.session_state.transcript_feed.insert(
|
| 1056 |
+
0,
|
| 1057 |
+
{
|
| 1058 |
+
"id": digest,
|
| 1059 |
+
"ts": int(time.time() * 1000),
|
| 1060 |
+
"text": full_text,
|
| 1061 |
+
},
|
| 1062 |
+
)
|
| 1063 |
+
st.session_state.edited_text = "\n\n".join(
|
| 1064 |
+
[s["text"] for s in st.session_state.transcript_feed]
|
| 1065 |
+
)
|
| 1066 |
+
st.success(f"Added new segment: {eff_start_ms/1000:.2f}s → {end_ms/1000:.2f}s")
|
| 1067 |
+
|
| 1068 |
+
# Now, asynchronously update translation and summary after segment is added
|
| 1069 |
+
def update_translation_and_summary():
|
| 1070 |
+
try:
|
| 1071 |
+
if full_text and st.session_state.get('enable_translation', True):
|
| 1072 |
+
translator = get_translator()
|
| 1073 |
+
sel_lang = st.session_state.get('broadcast_translation_lang', 'ar')
|
| 1074 |
+
tx, _ = translator.translate_text(full_text, target_language=sel_lang)
|
| 1075 |
+
if tx:
|
| 1076 |
+
seg['translations'][sel_lang] = tx
|
| 1077 |
+
except Exception:
|
| 1078 |
+
pass
|
| 1079 |
+
# Update summary
|
| 1080 |
+
if st.session_state.get('auto_generate_summary', True):
|
| 1081 |
+
try:
|
| 1082 |
+
source_text = " \n".join([s.get('text', '') for s in st.session_state.broadcast_segments if s.get('text')])
|
| 1083 |
+
if source_text.strip():
|
| 1084 |
+
summary, _ = generate_summary(source_text, target_language=st.session_state.get('summary_language', 'ar'))
|
| 1085 |
+
if summary:
|
| 1086 |
+
st.session_state.arabic_explanation = summary
|
| 1087 |
+
except Exception:
|
| 1088 |
+
pass
|
| 1089 |
+
import threading
|
| 1090 |
+
threading.Thread(target=update_translation_and_summary, daemon=True).start()
|
| 1091 |
+
else:
|
| 1092 |
+
st.info("Duplicate segment ignored.")
|
| 1093 |
+
else:
|
| 1094 |
+
st.info("No new parts after the last point.")
|
| 1095 |
+
else:
|
| 1096 |
+
# Legacy: treat as full wav bytes
|
| 1097 |
+
bytes_data = bytes(wav_audio_data)
|
| 1098 |
+
# This is not the Custom interval mode
|
| 1099 |
+
st.session_state['_custom_active'] = False
|
| 1100 |
+
st.session_state.audio_data = bytes_data
|
| 1101 |
+
st.audio(bytes_data)
|
| 1102 |
+
digest = hashlib.md5(bytes_data).hexdigest()
|
| 1103 |
+
last_digest = st.session_state.get('_last_component_digest')
|
| 1104 |
+
if st.session_state.auto_process_snapshots and digest != last_digest:
|
| 1105 |
+
st.session_state['_last_component_digest'] = digest
|
| 1106 |
+
task_id = run_audio_processing(bytes_data, "snapshot.wav")
|
| 1107 |
+
if task_id:
|
| 1108 |
+
st.success(f"🔄 {'تم إضافة المقطع للمعالجة' if st.session_state.language == 'ar' else 'Snapshot queued for processing'}")
|
| 1109 |
+
else:
|
| 1110 |
+
# Simple single button for processing
|
| 1111 |
+
if st.button("📝 " + ("استخراج النص" if st.session_state.language == 'ar' else "Extract Text"), type="primary", use_container_width=True):
|
| 1112 |
+
st.session_state['_last_component_digest'] = digest
|
| 1113 |
+
task_id = run_audio_processing(bytes_data, "recorded_audio.wav")
|
| 1114 |
+
if task_id:
|
| 1115 |
+
st.success(f"✅ {'تم إضافة التسجيل للمعالجة' if st.session_state.language == 'ar' else 'Audio queued for processing'}")
|
| 1116 |
+
|
| 1117 |
+
# Simplified: removed external live slice server UI to avoid complexity
|
| 1118 |
+
|
| 1119 |
+
# Always show Broadcast view in Step 1 as well (regardless of transcription_data)
|
| 1120 |
+
with st.expander("📻 Broadcast (latest first)", expanded=True):
|
| 1121 |
+
# Language selector for broadcast translations
|
| 1122 |
+
try:
|
| 1123 |
+
translator = get_translator()
|
| 1124 |
+
langs = translator.get_supported_languages()
|
| 1125 |
+
codes = list(langs.keys())
|
| 1126 |
+
labels = ["detect language — Arabic (العربية)"] + [f"{code} — {langs[code]}" for code in codes]
|
| 1127 |
+
current = st.session_state.get('broadcast_translation_lang', 'ar')
|
| 1128 |
+
# If not set, default to 'detect'
|
| 1129 |
+
if current not in codes and current != 'detect':
|
| 1130 |
+
current = 'detect'
|
| 1131 |
+
default_index = 0 if current == 'detect' else (codes.index(current) + 1 if current in codes else 1)
|
| 1132 |
+
sel_label = st.selectbox("Broadcast translation language", labels, index=default_index)
|
| 1133 |
+
if sel_label.startswith("detect language"):
|
| 1134 |
+
sel_code = 'detect'
|
| 1135 |
+
else:
|
| 1136 |
+
sel_code = sel_label.split(' — ')[0]
|
| 1137 |
+
st.session_state.broadcast_translation_lang = sel_code
|
| 1138 |
+
except Exception:
|
| 1139 |
+
sel_code = st.session_state.get('broadcast_translation_lang', 'ar')
|
| 1140 |
+
|
| 1141 |
+
if st.session_state.broadcast_segments:
|
| 1142 |
+
for s in sorted(st.session_state.broadcast_segments, key=lambda s: s['start_ms'], reverse=True):
|
| 1143 |
+
# Create unique segment ID
|
| 1144 |
+
segment_id = s.get('id', f"seg_{s['start_ms']}_{s['end_ms']}")
|
| 1145 |
+
|
| 1146 |
+
# Original text with selection capability
|
| 1147 |
+
original_text = s.get('text', '')
|
| 1148 |
+
if original_text:
|
| 1149 |
+
# Check if this segment is selected
|
| 1150 |
+
is_selected = (st.session_state.selected_segment_id == segment_id)
|
| 1151 |
+
|
| 1152 |
+
# Create timestamp for bubble
|
| 1153 |
+
timestamp = f"{s['start_ms']/1000:.1f}s → {s['end_ms']/1000:.1f}s"
|
| 1154 |
+
|
| 1155 |
+
# Create columns for bubble and ask button
|
| 1156 |
+
col_bubble, col_ask = st.columns([5, 1])
|
| 1157 |
+
|
| 1158 |
+
with col_bubble:
|
| 1159 |
+
# Display text as chat bubble
|
| 1160 |
+
bubble_html = create_broadcast_bubble(original_text, timestamp, is_selected)
|
| 1161 |
+
st.markdown(bubble_html, unsafe_allow_html=True)
|
| 1162 |
+
|
| 1163 |
+
if is_selected:
|
| 1164 |
+
st.success("🔍 " + ("هذا النص محدد للأسئلة" if st.session_state.language == 'ar' else "This text is selected for questions"))
|
| 1165 |
+
|
| 1166 |
+
with col_ask:
|
| 1167 |
+
# Ask AI button
|
| 1168 |
+
ask_button_text = "🤖 اسأل" if st.session_state.language == 'ar' else "🤖 Ask"
|
| 1169 |
+
button_type = "primary" if not is_selected else "secondary"
|
| 1170 |
+
if st.button(ask_button_text, key=f"ask_{segment_id}", type=button_type, use_container_width=True, help="اسأل الذكاء الاصطناعي عن هذا النص" if st.session_state.language == 'ar' else "Ask AI about this text"):
|
| 1171 |
+
# Select this text and open question modal
|
| 1172 |
+
st.session_state.selected_text = original_text
|
| 1173 |
+
st.session_state.selected_segment_id = segment_id
|
| 1174 |
+
st.session_state.show_question_modal = True
|
| 1175 |
+
st.rerun()
|
| 1176 |
+
|
| 1177 |
+
# Show model used for transcription
|
| 1178 |
+
model_note = s.get('transcription_model', None)
|
| 1179 |
+
if model_note:
|
| 1180 |
+
st.caption(f"Model used: {model_note}")
|
| 1181 |
+
|
| 1182 |
+
# Ensure and show translation in selected language
|
| 1183 |
+
if s.get('text') and st.session_state.get('enable_translation', True):
|
| 1184 |
+
if 'translations' not in s or not isinstance(s.get('translations'), dict):
|
| 1185 |
+
s['translations'] = {}
|
| 1186 |
+
# Detect language and translate if 'detect' is selected
|
| 1187 |
+
if sel_code == 'detect':
|
| 1188 |
+
# Use detected language from segment if available, else fallback to 'ar'
|
| 1189 |
+
detected_lang = s.get('detected_language', None)
|
| 1190 |
+
target_lang = 'ar' # Always translate to Arabic in detect mode
|
| 1191 |
+
if target_lang not in s['translations']:
|
| 1192 |
+
try:
|
| 1193 |
+
tx, _ = get_translator().translate_text(s.get('text', ''), target_language=target_lang)
|
| 1194 |
+
if tx:
|
| 1195 |
+
s['translations'][target_lang] = tx
|
| 1196 |
+
except Exception:
|
| 1197 |
+
pass
|
| 1198 |
+
if s['translations'].get(target_lang):
|
| 1199 |
+
st.caption(f"Translation (AR):")
|
| 1200 |
+
st.write(s['translations'][target_lang])
|
| 1201 |
+
else:
|
| 1202 |
+
if sel_code not in s['translations']:
|
| 1203 |
+
try:
|
| 1204 |
+
tx, _ = get_translator().translate_text(s.get('text', ''), target_language=sel_code)
|
| 1205 |
+
if tx:
|
| 1206 |
+
s['translations'][sel_code] = tx
|
| 1207 |
+
except Exception:
|
| 1208 |
+
pass
|
| 1209 |
+
if s['translations'].get(sel_code):
|
| 1210 |
+
st.caption(f"Translation ({sel_code.upper()}):")
|
| 1211 |
+
st.write(s['translations'][sel_code])
|
| 1212 |
+
st.divider()
|
| 1213 |
+
else:
|
| 1214 |
+
st.caption("No segments yet. Use the Custom button while recording.")
|
| 1215 |
+
|
| 1216 |
+
|
| 1217 |
+
|
| 1218 |
+
# --- Google Logout Function ---
|
| 1219 |
+
def logout_from_google():
|
| 1220 |
+
"""Logout from Google account"""
|
| 1221 |
+
try:
|
| 1222 |
+
success = google_docs_manager.logout()
|
| 1223 |
+
|
| 1224 |
+
if success:
|
| 1225 |
+
st.success("تم تسجيل الخروج بنجاح!" if st.session_state.language == 'ar' else "Successfully logged out!")
|
| 1226 |
+
st.info("يمكنك الآن تسجيل الدخول بحساب آخر" if st.session_state.language == 'ar' else "You can now login with a different account")
|
| 1227 |
+
# Force rerun to update UI
|
| 1228 |
+
time.sleep(1)
|
| 1229 |
+
st.rerun()
|
| 1230 |
+
else:
|
| 1231 |
+
st.error("خطأ في تسجيل الخروج" if st.session_state.language == 'ar' else "Error during logout")
|
| 1232 |
+
|
| 1233 |
+
except Exception as e:
|
| 1234 |
+
st.error(f"خطأ غير متوقع: {str(e)}" if st.session_state.language == 'ar' else f"Unexpected error: {str(e)}")
|
| 1235 |
+
|
| 1236 |
+
# --- Direct Google Docs Export Function ---
|
| 1237 |
+
def export_to_google_docs_directly():
|
| 1238 |
+
"""Export broadcast segments directly to Google Docs without conditions"""
|
| 1239 |
+
|
| 1240 |
+
try:
|
| 1241 |
+
# Get current segments (all segments, not filtered by timestamp)
|
| 1242 |
+
segments = st.session_state.broadcast_segments or []
|
| 1243 |
+
|
| 1244 |
+
# Show progress
|
| 1245 |
+
with st.spinner("جاري ��لتصدير إلى Google Docs..." if st.session_state.language == 'ar' else "Exporting to Google Docs..."):
|
| 1246 |
+
# Export directly
|
| 1247 |
+
doc_url, error = google_docs_manager.export_broadcast_to_docs(
|
| 1248 |
+
segments,
|
| 1249 |
+
ui_language=st.session_state.language
|
| 1250 |
+
)
|
| 1251 |
+
|
| 1252 |
+
if doc_url and not error:
|
| 1253 |
+
st.success("تم إنشاء المستند بنجاح!" if st.session_state.language == 'ar' else "Document created successfully!")
|
| 1254 |
+
|
| 1255 |
+
# Show clickable link
|
| 1256 |
+
if st.session_state.language == 'ar':
|
| 1257 |
+
st.markdown(f"🔗 [فتح المستند في Google Docs]({doc_url})")
|
| 1258 |
+
st.info("💡 نصيحة: اضغط على الرابط أعلاه لفتح المستند في تبويب جديد")
|
| 1259 |
+
else:
|
| 1260 |
+
st.markdown(f"🔗 [Open Document in Google Docs]({doc_url})")
|
| 1261 |
+
st.info("💡 Tip: Click the link above to open the document in a new tab")
|
| 1262 |
+
|
| 1263 |
+
# Also show the URL for copying
|
| 1264 |
+
st.code(doc_url, language=None)
|
| 1265 |
+
|
| 1266 |
+
# Show current user info
|
| 1267 |
+
if google_docs_manager.is_authenticated():
|
| 1268 |
+
st.caption("✅ متصل بحساب Google" if st.session_state.language == 'ar' else "✅ Connected to Google account")
|
| 1269 |
+
|
| 1270 |
+
else:
|
| 1271 |
+
error_msg = error or "Unknown error occurred"
|
| 1272 |
+
st.error(f"خطأ في التصدير: {error_msg}" if st.session_state.language == 'ar' else f"Export error: {error_msg}")
|
| 1273 |
+
|
| 1274 |
+
# Show setup instructions if credentials are missing
|
| 1275 |
+
if "credentials" in error_msg.lower() or "authentication" in error_msg.lower():
|
| 1276 |
+
st.info("📋 يرجى مراجعة ملف GOOGLE_SETUP.md لإعداد Google Docs" if st.session_state.language == 'ar' else "📋 Please check GOOGLE_SETUP.md for Google Docs setup instructions")
|
| 1277 |
+
|
| 1278 |
+
except Exception as e:
|
| 1279 |
+
st.error(f"خطأ غير متوقع: {str(e)}" if st.session_state.language == 'ar' else f"Unexpected error: {str(e)}")
|
| 1280 |
+
|
| 1281 |
+
# --- AI Question Modal Function ---
|
| 1282 |
+
def show_question_modal():
|
| 1283 |
+
"""Display AI question modal for selected text"""
|
| 1284 |
+
|
| 1285 |
+
if not st.session_state.selected_text:
|
| 1286 |
+
st.session_state.show_question_modal = False
|
| 1287 |
+
return
|
| 1288 |
+
|
| 1289 |
+
# Get AI question engine
|
| 1290 |
+
question_engine = get_ai_question_engine()
|
| 1291 |
+
|
| 1292 |
+
# Modal header
|
| 1293 |
+
st.subheader("🤖 اسأل الذكاء الاصطناعي" if st.session_state.language == 'ar' else "🤖 Ask AI")
|
| 1294 |
+
|
| 1295 |
+
# Show selected text
|
| 1296 |
+
with st.expander("النص المحدد" if st.session_state.language == 'ar' else "Selected Text", expanded=True):
|
| 1297 |
+
st.write(f"📝 {st.session_state.selected_text}")
|
| 1298 |
+
|
| 1299 |
+
# Show conversation history if exists
|
| 1300 |
+
if st.session_state.current_question_session:
|
| 1301 |
+
session = question_engine.get_conversation_history(st.session_state.current_question_session)
|
| 1302 |
+
if session and session.conversation:
|
| 1303 |
+
with st.expander(f"💬 تاريخ المحادثة ({len(session.conversation)} أسئلة)" if st.session_state.language == 'ar' else f"💬 Conversation History ({len(session.conversation)} questions)", expanded=False):
|
| 1304 |
+
for i, qa in enumerate(session.conversation, 1):
|
| 1305 |
+
st.markdown(f"**{i}. {qa.question}**")
|
| 1306 |
+
st.write(qa.answer)
|
| 1307 |
+
|
| 1308 |
+
# Show timing and model info
|
| 1309 |
+
model_info = getattr(qa, 'model_used', 'Unknown')
|
| 1310 |
+
model_color = "green" if "Gemini" in model_info else "orange" if "Groq" in model_info or "OpenRouter" in model_info else "red"
|
| 1311 |
+
|
| 1312 |
+
caption_text = f"⏱️ {qa.timestamp.strftime('%H:%M:%S')} - {qa.response_time_ms}ms"
|
| 1313 |
+
model_text = f"🔧 {model_info}"
|
| 1314 |
+
|
| 1315 |
+
st.caption(caption_text)
|
| 1316 |
+
st.markdown(f"<small style='color: {model_color}'>{model_text}</small>", unsafe_allow_html=True)
|
| 1317 |
+
|
| 1318 |
+
if i < len(session.conversation):
|
| 1319 |
+
st.divider()
|
| 1320 |
+
|
| 1321 |
+
# Model selection (moved to top)
|
| 1322 |
+
st.markdown("**" + ("اختيار النموذج" if st.session_state.language == 'ar' else "Model Selection") + "**")
|
| 1323 |
+
|
| 1324 |
+
# Get model availability
|
| 1325 |
+
models_status = question_engine.check_model_availability()
|
| 1326 |
+
|
| 1327 |
+
# Create model options with status indicators
|
| 1328 |
+
model_options = {}
|
| 1329 |
+
for model_name, status_info in models_status.items():
|
| 1330 |
+
display_name = f"{status_info['icon']} {model_name} - {status_info['message']}"
|
| 1331 |
+
model_options[display_name] = model_name
|
| 1332 |
+
|
| 1333 |
+
# Add auto option
|
| 1334 |
+
auto_text = "🔄 تلقائي (أفضل نموذج متاح)" if st.session_state.language == 'ar' else "🔄 Auto (Best available model)"
|
| 1335 |
+
model_options = {auto_text: 'auto', **model_options}
|
| 1336 |
+
|
| 1337 |
+
# Find current selection index
|
| 1338 |
+
current_model = st.session_state.get('preferred_ai_model', 'auto')
|
| 1339 |
+
current_index = 0
|
| 1340 |
+
for i, (display_name, model_name) in enumerate(model_options.items()):
|
| 1341 |
+
if model_name == current_model:
|
| 1342 |
+
current_index = i
|
| 1343 |
+
break
|
| 1344 |
+
|
| 1345 |
+
# Model selector
|
| 1346 |
+
selected_model_display = st.selectbox(
|
| 1347 |
+
"النموذج المفضل" if st.session_state.language == 'ar' else "Preferred Model",
|
| 1348 |
+
options=list(model_options.keys()),
|
| 1349 |
+
index=current_index,
|
| 1350 |
+
help="اختر النموذج المفضل للإجابة" if st.session_state.language == 'ar' else "Choose preferred model for answering"
|
| 1351 |
+
)
|
| 1352 |
+
|
| 1353 |
+
selected_model = model_options[selected_model_display]
|
| 1354 |
+
st.session_state.preferred_ai_model = selected_model
|
| 1355 |
+
|
| 1356 |
+
# Show model status details
|
| 1357 |
+
if selected_model != 'auto':
|
| 1358 |
+
status_info = models_status[selected_model]
|
| 1359 |
+
if status_info['status'] != 'available':
|
| 1360 |
+
if status_info['status'] == 'quota_exceeded':
|
| 1361 |
+
st.warning("⚠️ هذا النموذج استنفد حصته اليومية" if st.session_state.language == 'ar' else "⚠️ This model has exceeded its daily quota")
|
| 1362 |
+
elif status_info['status'] == 'not_configured':
|
| 1363 |
+
st.info("ℹ️ هذا النموذج غير مُعد - سيتم استخدام البديل" if st.session_state.language == 'ar' else "ℹ️ This model is not configured - fallback will be used")
|
| 1364 |
+
else:
|
| 1365 |
+
st.error("❌ هذا النموذج غير متاح حالياً" if st.session_state.language == 'ar' else "❌ This model is currently unavailable")
|
| 1366 |
+
|
| 1367 |
+
# Language selection for answers
|
| 1368 |
+
st.markdown("**" + ("لغة الإجابة" if st.session_state.language == 'ar' else "Answer Language") + "**")
|
| 1369 |
+
|
| 1370 |
+
# Language options
|
| 1371 |
+
language_options = {
|
| 1372 |
+
"🔄 تلقائي (حسب لغة الواجهة)" if st.session_state.language == 'ar' else "🔄 Auto (Interface language)": 'auto',
|
| 1373 |
+
"🇸🇦 العربية": 'ar',
|
| 1374 |
+
"🇺🇸 English": 'en',
|
| 1375 |
+
"🇫🇷 Français": 'fr',
|
| 1376 |
+
"🇪🇸 Español": 'es',
|
| 1377 |
+
"🇩🇪 Deutsch": 'de',
|
| 1378 |
+
"🇨🇳 中文": 'zh'
|
| 1379 |
+
}
|
| 1380 |
+
|
| 1381 |
+
# Find current language selection
|
| 1382 |
+
current_lang = st.session_state.get('preferred_answer_language', 'auto')
|
| 1383 |
+
current_lang_index = 0
|
| 1384 |
+
for i, (display_name, lang_code) in enumerate(language_options.items()):
|
| 1385 |
+
if lang_code == current_lang:
|
| 1386 |
+
current_lang_index = i
|
| 1387 |
+
break
|
| 1388 |
+
|
| 1389 |
+
# Language selector
|
| 1390 |
+
selected_language_display = st.selectbox(
|
| 1391 |
+
"لغة الإجابة المفضلة" if st.session_state.language == 'ar' else "Preferred Answer Language",
|
| 1392 |
+
options=list(language_options.keys()),
|
| 1393 |
+
index=current_lang_index,
|
| 1394 |
+
help="اختر اللغة التي تريد الحصول على الإجابة بها" if st.session_state.language == 'ar' else "Choose the language for AI responses"
|
| 1395 |
+
)
|
| 1396 |
+
|
| 1397 |
+
selected_answer_language = language_options[selected_language_display]
|
| 1398 |
+
st.session_state.preferred_answer_language = selected_answer_language
|
| 1399 |
+
|
| 1400 |
+
# Show language info
|
| 1401 |
+
if selected_answer_language == 'auto':
|
| 1402 |
+
current_ui_lang = "العربية" if st.session_state.language == 'ar' else "English"
|
| 1403 |
+
st.caption(f"ℹ️ سيتم استخدام لغة الواجهة الحالية: {current_ui_lang}" if st.session_state.language == 'ar' else f"ℹ️ Will use current interface language: {current_ui_lang}")
|
| 1404 |
+
else:
|
| 1405 |
+
lang_names = {'ar': 'العربية', 'en': 'English', 'fr': 'Français', 'es': 'Español', 'de': 'Deutsch', 'zh': '中文'}
|
| 1406 |
+
selected_lang_name = lang_names.get(selected_answer_language, selected_answer_language)
|
| 1407 |
+
st.caption(f"ℹ️ الإجابات ستكون باللغة: {selected_lang_name}" if st.session_state.language == 'ar' else f"ℹ️ Answers will be in: {selected_lang_name}")
|
| 1408 |
+
|
| 1409 |
+
# Question templates
|
| 1410 |
+
st.markdown("**" + ("قوالب الأسئلة السريعة" if st.session_state.language == 'ar' else "Quick Question Templates") + "**")
|
| 1411 |
+
|
| 1412 |
+
templates = question_engine.get_question_templates(st.session_state.language)
|
| 1413 |
+
|
| 1414 |
+
# Display templates as buttons in columns
|
| 1415 |
+
cols = st.columns(2)
|
| 1416 |
+
for i, template in enumerate(templates[:6]): # Show first 6 templates
|
| 1417 |
+
with cols[i % 2]:
|
| 1418 |
+
if st.button(template, key=f"template_{i}", use_container_width=True):
|
| 1419 |
+
# Process template question
|
| 1420 |
+
process_ai_question(template, is_template=True, preferred_model=selected_model, answer_language=selected_answer_language)
|
| 1421 |
+
return
|
| 1422 |
+
|
| 1423 |
+
# Custom question input
|
| 1424 |
+
st.markdown("**" + ("أو اكتب سؤالك الخاص" if st.session_state.language == 'ar' else "Or Write Your Own Question") + "**")
|
| 1425 |
+
|
| 1426 |
+
custom_question = st.text_area(
|
| 1427 |
+
"سؤالك" if st.session_state.language == 'ar' else "Your Question",
|
| 1428 |
+
placeholder="اكتب سؤالك هنا..." if st.session_state.language == 'ar' else "Type your question here...",
|
| 1429 |
+
height=100
|
| 1430 |
+
)
|
| 1431 |
+
|
| 1432 |
+
# Action buttons
|
| 1433 |
+
col_ask, col_cancel = st.columns(2)
|
| 1434 |
+
|
| 1435 |
+
with col_ask:
|
| 1436 |
+
if st.button("🚀 اسأل" if st.session_state.language == 'ar' else "🚀 Ask", type="primary", disabled=not custom_question.strip()):
|
| 1437 |
+
if custom_question.strip():
|
| 1438 |
+
process_ai_question(custom_question.strip(), is_template=False, preferred_model=selected_model, answer_language=selected_answer_language)
|
| 1439 |
+
return
|
| 1440 |
+
|
| 1441 |
+
with col_cancel:
|
| 1442 |
+
if st.button("❌ إلغاء" if st.session_state.language == 'ar' else "❌ Cancel"):
|
| 1443 |
+
st.session_state.show_question_modal = False
|
| 1444 |
+
st.session_state.selected_text = None
|
| 1445 |
+
st.session_state.selected_segment_id = None
|
| 1446 |
+
st.rerun()
|
| 1447 |
+
|
| 1448 |
+
def process_ai_question(question: str, is_template: bool = False, preferred_model: str = 'auto', answer_language: str = 'auto'):
|
| 1449 |
+
"""Process AI question and show response"""
|
| 1450 |
+
|
| 1451 |
+
question_engine = get_ai_question_engine()
|
| 1452 |
+
|
| 1453 |
+
# Prepare segment info
|
| 1454 |
+
segment_info = {
|
| 1455 |
+
'id': st.session_state.selected_segment_id,
|
| 1456 |
+
'start_ms': 0, # We'll get this from the actual segment if needed
|
| 1457 |
+
'end_ms': 0
|
| 1458 |
+
}
|
| 1459 |
+
|
| 1460 |
+
# Show processing indicator
|
| 1461 |
+
with st.spinner("جاري معالجة سؤالك..." if st.session_state.language == 'ar' else "Processing your question..."):
|
| 1462 |
+
# Determine answer language
|
| 1463 |
+
if answer_language == 'auto':
|
| 1464 |
+
answer_lang = st.session_state.language
|
| 1465 |
+
else:
|
| 1466 |
+
answer_lang = answer_language
|
| 1467 |
+
|
| 1468 |
+
# Process question
|
| 1469 |
+
result = question_engine.process_question(
|
| 1470 |
+
selected_text=st.session_state.selected_text,
|
| 1471 |
+
question=question,
|
| 1472 |
+
segment_info=segment_info,
|
| 1473 |
+
ui_language=answer_lang, # Use selected answer language
|
| 1474 |
+
session_id=st.session_state.current_question_session,
|
| 1475 |
+
preferred_model=preferred_model
|
| 1476 |
+
)
|
| 1477 |
+
|
| 1478 |
+
# Handle different return formats for backward compatibility
|
| 1479 |
+
if len(result) == 4:
|
| 1480 |
+
answer, error, session_id, model_used = result
|
| 1481 |
+
else:
|
| 1482 |
+
answer, error, session_id = result
|
| 1483 |
+
model_used = "Unknown"
|
| 1484 |
+
|
| 1485 |
+
# Update session ID
|
| 1486 |
+
st.session_state.current_question_session = session_id
|
| 1487 |
+
|
| 1488 |
+
if answer:
|
| 1489 |
+
# Check response type and model fallback
|
| 1490 |
+
is_simple_response = "ملاحظة: هذه إجابة مبسطة" in answer or "Note: This is a simplified response" in answer
|
| 1491 |
+
preferred_model = st.session_state.get('preferred_ai_model', 'auto')
|
| 1492 |
+
model_fallback = preferred_model != 'auto' and preferred_model != model_used
|
| 1493 |
+
|
| 1494 |
+
if is_simple_response:
|
| 1495 |
+
st.warning("⚠️ خدمة الذكاء الاصطناعي غير متاحة حالياً - إجابة مبسطة" if st.session_state.language == 'ar' else "⚠️ AI service temporarily unavailable - simplified response")
|
| 1496 |
+
elif model_fallback:
|
| 1497 |
+
st.info(f"ℹ️ النموذج المفضل ({preferred_model}) غير متاح - تم استخدام {model_used}" if st.session_state.language == 'ar' else f"ℹ️ Preferred model ({preferred_model}) unavailable - used {model_used}")
|
| 1498 |
+
else:
|
| 1499 |
+
st.success("تم الحصول على الإجابة!" if st.session_state.language == 'ar' else "Got the answer!")
|
| 1500 |
+
|
| 1501 |
+
# Display Q&A
|
| 1502 |
+
st.markdown("### " + ("السؤال" if st.session_state.language == 'ar' else "Question"))
|
| 1503 |
+
st.write(f"❓ {question}")
|
| 1504 |
+
|
| 1505 |
+
st.markdown("### " + ("الإجابة" if st.session_state.language == 'ar' else "Answer"))
|
| 1506 |
+
st.write(f"🤖 {answer}")
|
| 1507 |
+
|
| 1508 |
+
# Show which model was used with enhanced styling
|
| 1509 |
+
if model_used:
|
| 1510 |
+
# Get model status for better display
|
| 1511 |
+
question_engine = get_ai_question_engine()
|
| 1512 |
+
models_status = question_engine.check_model_availability()
|
| 1513 |
+
|
| 1514 |
+
model_info = models_status.get(model_used, {})
|
| 1515 |
+
icon = model_info.get('icon', '🤖')
|
| 1516 |
+
color = model_info.get('color', 'gray')
|
| 1517 |
+
|
| 1518 |
+
# Show if user's preferred model was used or fallback occurred
|
| 1519 |
+
preferred_model = st.session_state.get('preferred_ai_model', 'auto')
|
| 1520 |
+
if preferred_model != 'auto' and preferred_model != model_used:
|
| 1521 |
+
fallback_msg = " (تم التبديل للبديل)" if st.session_state.language == 'ar' else " (fallback used)"
|
| 1522 |
+
color = "orange"
|
| 1523 |
+
else:
|
| 1524 |
+
fallback_msg = ""
|
| 1525 |
+
|
| 1526 |
+
model_display = f"{icon} {model_used}{fallback_msg}"
|
| 1527 |
+
|
| 1528 |
+
# Show answer language info
|
| 1529 |
+
answer_lang = st.session_state.get('preferred_answer_language', 'auto')
|
| 1530 |
+
if answer_lang == 'auto':
|
| 1531 |
+
lang_display = "تلقائي" if st.session_state.language == 'ar' else "Auto"
|
| 1532 |
+
lang_flag = "🔄"
|
| 1533 |
+
else:
|
| 1534 |
+
lang_flags = {'ar': '🇸🇦', 'en': '🇺🇸', 'fr': '🇫🇷', 'es': '🇪🇸', 'de': '🇩🇪', 'zh': '🇨🇳'}
|
| 1535 |
+
lang_names = {'ar': 'العربية', 'en': 'English', 'fr': 'Français', 'es': 'Español', 'de': 'Deutsch', 'zh': '中文'}
|
| 1536 |
+
lang_flag = lang_flags.get(answer_lang, '🌐')
|
| 1537 |
+
lang_display = lang_names.get(answer_lang, answer_lang)
|
| 1538 |
+
|
| 1539 |
+
info_text = f"🔧 {'النموذج' if st.session_state.language == 'ar' else 'Model'}: {model_display} | 🌐 {'اللغة' if st.session_state.language == 'ar' else 'Language'}: {lang_flag} {lang_display}"
|
| 1540 |
+
|
| 1541 |
+
st.markdown(f"<div style='background-color: rgba(0,0,0,0.1); padding: 8px; border-radius: 5px; margin: 5px 0;'><small style='color: {color}'>{info_text}</small></div>", unsafe_allow_html=True)
|
| 1542 |
+
|
| 1543 |
+
# Show additional help for simple responses
|
| 1544 |
+
if is_simple_response:
|
| 1545 |
+
with st.expander("💡 نصائح للحصول على إجابات أفضل" if st.session_state.language == 'ar' else "💡 Tips for better answers"):
|
| 1546 |
+
if st.session_state.language == 'ar':
|
| 1547 |
+
st.markdown("""
|
| 1548 |
+
**لماذا الإجابة مبسطة؟**
|
| 1549 |
+
- تم استنفاد الحد اليومي لخدمة Gemini AI المجانية (50 طلب/يوم)
|
| 1550 |
+
- النظام يستخدم إجابات مبسطة كبديل مؤقت
|
| 1551 |
+
|
| 1552 |
+
**للحصول على إجابات أفضل:**
|
| 1553 |
+
- حاول مرة أخرى غداً (يتم تجديد الحد اليومي)
|
| 1554 |
+
- اطرح أسئلة أكثر تحديداً
|
| 1555 |
+
- ابحث في مصادر إضافية للموضوع
|
| 1556 |
+
""")
|
| 1557 |
+
else:
|
| 1558 |
+
st.markdown("""
|
| 1559 |
+
**Why is the answer simplified?**
|
| 1560 |
+
- Daily limit for free Gemini AI service exceeded (50 requests/day)
|
| 1561 |
+
- System is using simplified responses as temporary fallback
|
| 1562 |
+
|
| 1563 |
+
**For better answers:**
|
| 1564 |
+
- Try again tomorrow (daily limit resets)
|
| 1565 |
+
- Ask more specific questions
|
| 1566 |
+
- Search additional sources for the topic
|
| 1567 |
+
""")
|
| 1568 |
+
|
| 1569 |
+
|
| 1570 |
+
# Action buttons for the response
|
| 1571 |
+
col_copy, col_follow, col_close = st.columns(3)
|
| 1572 |
+
|
| 1573 |
+
with col_copy:
|
| 1574 |
+
if st.button("📋 نسخ" if st.session_state.language == 'ar' else "📋 Copy"):
|
| 1575 |
+
# Format for copying
|
| 1576 |
+
copy_text = f"السؤال: {question}\nالإجابة: {answer}" if st.session_state.language == 'ar' else f"Question: {question}\nAnswer: {answer}"
|
| 1577 |
+
st.code(copy_text, language=None)
|
| 1578 |
+
st.success("تم تنسيق النص للنسخ أعلاه" if st.session_state.language == 'ar' else "Text formatted for copying above")
|
| 1579 |
+
|
| 1580 |
+
with col_follow:
|
| 1581 |
+
if st.button("➕ سؤال متابعة" if st.session_state.language == 'ar' else "➕ Follow-up"):
|
| 1582 |
+
# Keep modal open for follow-up question
|
| 1583 |
+
st.rerun()
|
| 1584 |
+
|
| 1585 |
+
with col_close:
|
| 1586 |
+
if st.button("✅ إغلاق" if st.session_state.language == 'ar' else "✅ Close"):
|
| 1587 |
+
st.session_state.show_question_modal = False
|
| 1588 |
+
st.session_state.selected_text = None
|
| 1589 |
+
st.session_state.selected_segment_id = None
|
| 1590 |
+
st.rerun()
|
| 1591 |
+
|
| 1592 |
+
else:
|
| 1593 |
+
# Show error
|
| 1594 |
+
st.error(f"خطأ: {error}" if st.session_state.language == 'ar' else f"Error: {error}")
|
| 1595 |
+
|
| 1596 |
+
# Retry and close buttons
|
| 1597 |
+
col_retry, col_close = st.columns(2)
|
| 1598 |
+
|
| 1599 |
+
with col_retry:
|
| 1600 |
+
if st.button("🔄 إعادة المحاولة" if st.session_state.language == 'ar' else "🔄 Retry"):
|
| 1601 |
+
process_ai_question(question, is_template)
|
| 1602 |
+
return
|
| 1603 |
+
|
| 1604 |
+
with col_close:
|
| 1605 |
+
if st.button("❌ إغلاق" if st.session_state.language == 'ar' else "❌ Close"):
|
| 1606 |
+
st.session_state.show_question_modal = False
|
| 1607 |
+
st.session_state.selected_text = None
|
| 1608 |
+
st.session_state.selected_segment_id = None
|
| 1609 |
+
st.rerun()
|
| 1610 |
+
|
| 1611 |
+
# --- Export Modal Function ---
|
| 1612 |
+
def show_export_modal():
|
| 1613 |
+
"""Display export modal with preview and options"""
|
| 1614 |
+
|
| 1615 |
+
# Initialize Google Docs auth
|
| 1616 |
+
if 'google_auth' not in st.session_state:
|
| 1617 |
+
st.session_state.google_auth = GoogleDocsAuth()
|
| 1618 |
+
|
| 1619 |
+
google_auth = st.session_state.google_auth
|
| 1620 |
+
|
| 1621 |
+
# Filter segments from export timestamp
|
| 1622 |
+
if not st.session_state.export_timestamp or not st.session_state.broadcast_segments:
|
| 1623 |
+
st.session_state.show_export_modal = False
|
| 1624 |
+
return
|
| 1625 |
+
|
| 1626 |
+
# Get segments after export timestamp
|
| 1627 |
+
filtered_segments = []
|
| 1628 |
+
for segment in st.session_state.broadcast_segments:
|
| 1629 |
+
if segment.get('start_ms', 0) >= st.session_state.export_timestamp:
|
| 1630 |
+
filtered_segments.append(segment)
|
| 1631 |
+
|
| 1632 |
+
# Sort by start time (oldest first for export)
|
| 1633 |
+
filtered_segments.sort(key=lambda s: s.get('start_ms', 0))
|
| 1634 |
+
|
| 1635 |
+
if not filtered_segments:
|
| 1636 |
+
st.warning("لا توجد مقاطع جديدة للتصدير منذ الضغط على الزر" if st.session_state.language == 'ar' else "No new segments to export since button press")
|
| 1637 |
+
if st.button("إغلاق" if st.session_state.language == 'ar' else "Close"):
|
| 1638 |
+
st.session_state.show_export_modal = False
|
| 1639 |
+
st.rerun()
|
| 1640 |
+
return
|
| 1641 |
+
|
| 1642 |
+
# Export preview
|
| 1643 |
+
st.subheader("📋 معاينة التصدير" if st.session_state.language == 'ar' else "📋 Export Preview")
|
| 1644 |
+
|
| 1645 |
+
export_time = datetime.fromtimestamp(st.session_state.export_timestamp / 1000)
|
| 1646 |
+
st.info(f"{'المقاطع من وقت' if st.session_state.language == 'ar' else 'Segments from'}: {export_time.strftime('%H:%M:%S')}")
|
| 1647 |
+
st.info(f"{'عدد المقاطع' if st.session_state.language == 'ar' else 'Number of segments'}: {len(filtered_segments)}")
|
| 1648 |
+
|
| 1649 |
+
# Show preview of segments
|
| 1650 |
+
with st.expander("معاينة المحتوى" if st.session_state.language == 'ar' else "Content Preview", expanded=False):
|
| 1651 |
+
for i, segment in enumerate(filtered_segments[:3]): # Show first 3 segments
|
| 1652 |
+
start_time = segment.get('start_ms', 0) / 1000
|
| 1653 |
+
end_time = segment.get('end_ms', 0) / 1000
|
| 1654 |
+
st.markdown(f"**[{start_time:.2f}s → {end_time:.2f}s]**")
|
| 1655 |
+
st.write(segment.get('text', '')[:100] + "..." if len(segment.get('text', '')) > 100 else segment.get('text', ''))
|
| 1656 |
+
if i < 2 and i < len(filtered_segments) - 1:
|
| 1657 |
+
st.divider()
|
| 1658 |
+
|
| 1659 |
+
if len(filtered_segments) > 3:
|
| 1660 |
+
st.caption(f"... {'و' if st.session_state.language == 'ar' else 'and'} {len(filtered_segments) - 3} {'مقاطع أخرى' if st.session_state.language == 'ar' else 'more segments'}")
|
| 1661 |
+
|
| 1662 |
+
# Export options
|
| 1663 |
+
col1, col2 = st.columns(2)
|
| 1664 |
+
|
| 1665 |
+
with col1:
|
| 1666 |
+
format_options = {
|
| 1667 |
+
"📄 Word Document": "word",
|
| 1668 |
+
"📝 Google Docs": "google_docs"
|
| 1669 |
+
}
|
| 1670 |
+
selected_format = st.selectbox(
|
| 1671 |
+
"تنسيق التصدير" if st.session_state.language == 'ar' else "Export Format",
|
| 1672 |
+
options=list(format_options.keys()),
|
| 1673 |
+
index=0
|
| 1674 |
+
)
|
| 1675 |
+
st.session_state.export_format = format_options[selected_format]
|
| 1676 |
+
|
| 1677 |
+
with col2:
|
| 1678 |
+
include_summary = st.checkbox(
|
| 1679 |
+
"تضمين الملخص" if st.session_state.language == 'ar' else "Include Summary",
|
| 1680 |
+
value=True
|
| 1681 |
+
)
|
| 1682 |
+
|
| 1683 |
+
# Google Docs authentication section
|
| 1684 |
+
if st.session_state.export_format == 'google_docs':
|
| 1685 |
+
st.markdown("---")
|
| 1686 |
+
if google_auth.is_authenticated():
|
| 1687 |
+
st.success("✅ " + ("متصل بـ Google Docs" if st.session_state.language == 'ar' else "Connected to Google Docs"))
|
| 1688 |
+
col_logout, col_info = st.columns([1, 2])
|
| 1689 |
+
with col_logout:
|
| 1690 |
+
if st.button("🚪 " + ("تسجيل خروج" if st.session_state.language == 'ar' else "Logout")):
|
| 1691 |
+
google_auth.logout()
|
| 1692 |
+
st.rerun()
|
| 1693 |
+
with col_info:
|
| 1694 |
+
st.caption("سيتم إنشاء المستند في حسابك على Google" if st.session_state.language == 'ar' else "Document will be created in your Google account")
|
| 1695 |
+
else:
|
| 1696 |
+
st.warning("🔐 " + ("يجب تسجيل الدخول إلى Google Docs أولاً" if st.session_state.language == 'ar' else "Please authenticate with Google Docs first"))
|
| 1697 |
+
|
| 1698 |
+
# Handle OAuth callback
|
| 1699 |
+
auth_code = st.query_params.get("code")
|
| 1700 |
+
if auth_code:
|
| 1701 |
+
success, message = google_auth.handle_auth_callback(auth_code)
|
| 1702 |
+
if success:
|
| 1703 |
+
st.success(message)
|
| 1704 |
+
# Clear the code from URL
|
| 1705 |
+
st.query_params.clear()
|
| 1706 |
+
st.rerun()
|
| 1707 |
+
else:
|
| 1708 |
+
st.error(message)
|
| 1709 |
+
|
| 1710 |
+
# Show authentication button
|
| 1711 |
+
auth_url, error = google_auth.get_auth_url()
|
| 1712 |
+
if auth_url:
|
| 1713 |
+
st.markdown(f"""
|
| 1714 |
+
<a href="{auth_url}" target="_blank">
|
| 1715 |
+
<button style="background-color: #4285f4; color: white; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer;">
|
| 1716 |
+
🔗 {"تسجيل الدخول إلى Google" if st.session_state.language == 'ar' else "Sign in with Google"}
|
| 1717 |
+
</button>
|
| 1718 |
+
</a>
|
| 1719 |
+
""", unsafe_allow_html=True)
|
| 1720 |
+
st.caption("سيتم فتح نافذة جديدة للمصادقة" if st.session_state.language == 'ar' else "A new window will open for authentication")
|
| 1721 |
+
else:
|
| 1722 |
+
st.error(f"خطأ في إعداد Google API: {error}" if st.session_state.language == 'ar' else f"Google API setup error: {error}")
|
| 1723 |
+
st.info("يرجى إعداد GOOGLE_CLIENT_ID و GOOGLE_CLIENT_SECRET في متغيرات البيئة" if st.session_state.language == 'ar' else "Please set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET environment variables")
|
| 1724 |
+
|
| 1725 |
+
# Export buttons
|
| 1726 |
+
col_export, col_cancel = st.columns(2)
|
| 1727 |
+
|
| 1728 |
+
with col_export:
|
| 1729 |
+
# Disable export button if Google Docs is selected but not authenticated
|
| 1730 |
+
export_disabled = (st.session_state.export_format == 'google_docs' and not google_auth.is_authenticated())
|
| 1731 |
+
export_button_text = "🚀 تصدير" if st.session_state.language == 'ar' else "🚀 Export"
|
| 1732 |
+
|
| 1733 |
+
if st.button(export_button_text, type="primary", disabled=export_disabled):
|
| 1734 |
+
perform_export(filtered_segments, include_summary, google_auth)
|
| 1735 |
+
|
| 1736 |
+
with col_cancel:
|
| 1737 |
+
if st.button("❌ إلغاء" if st.session_state.language == 'ar' else "❌ Cancel"):
|
| 1738 |
+
st.session_state.show_export_modal = False
|
| 1739 |
+
st.rerun()
|
| 1740 |
+
|
| 1741 |
+
# --- Export Execution Function ---
|
| 1742 |
+
def perform_export(segments, include_summary=True, google_auth=None):
|
| 1743 |
+
"""Perform the actual export operation"""
|
| 1744 |
+
|
| 1745 |
+
try:
|
| 1746 |
+
# Initialize exporter
|
| 1747 |
+
translator = get_translator()
|
| 1748 |
+
exporter = BroadcastExporter(translator)
|
| 1749 |
+
|
| 1750 |
+
# Create export configuration
|
| 1751 |
+
config = ExportConfig(
|
| 1752 |
+
export_timestamp=st.session_state.export_timestamp,
|
| 1753 |
+
format_type=st.session_state.export_format,
|
| 1754 |
+
include_summary=include_summary,
|
| 1755 |
+
ui_language=st.session_state.language,
|
| 1756 |
+
target_language=st.session_state.get('broadcast_translation_lang', 'ar')
|
| 1757 |
+
)
|
| 1758 |
+
|
| 1759 |
+
# Prepare export content
|
| 1760 |
+
content = exporter.prepare_export_content(segments, config)
|
| 1761 |
+
|
| 1762 |
+
# Show progress
|
| 1763 |
+
with st.spinner("جاري التصدير..." if st.session_state.language == 'ar' else "Exporting..."):
|
| 1764 |
+
# Perform export with fallback
|
| 1765 |
+
result, error = exporter.export_with_fallback(content, config, google_auth)
|
| 1766 |
+
|
| 1767 |
+
if result and not error:
|
| 1768 |
+
if config.format_type == 'word':
|
| 1769 |
+
# Provide download link for Word document
|
| 1770 |
+
with open(result, 'rb') as file:
|
| 1771 |
+
st.download_button(
|
| 1772 |
+
label="📥 تحميل الملف" if st.session_state.language == 'ar' else "📥 Download File",
|
| 1773 |
+
data=file.read(),
|
| 1774 |
+
file_name=os.path.basename(result),
|
| 1775 |
+
mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
| 1776 |
+
)
|
| 1777 |
+
st.success("تم إنشاء الملف بنجاح!" if st.session_state.language == 'ar' else "File created successfully!")
|
| 1778 |
+
else:
|
| 1779 |
+
# Google Docs URL
|
| 1780 |
+
st.success("تم إنشاء المستند بنجاح!" if st.session_state.language == 'ar' else "Document created successfully!")
|
| 1781 |
+
st.markdown(f"[فتح في Google Docs]({result})" if st.session_state.language == 'ar' else f"[Open in Google Docs]({result})")
|
| 1782 |
+
else:
|
| 1783 |
+
st.error(f"خطأ في التصدير: {error}" if st.session_state.language == 'ar' else f"Export error: {error}")
|
| 1784 |
+
|
| 1785 |
+
except Exception as e:
|
| 1786 |
+
st.error(f"خطأ غير متوقع: {str(e)}" if st.session_state.language == 'ar' else f"Unexpected error: {str(e)}")
|
| 1787 |
+
|
| 1788 |
+
# Close modal after export attempt
|
| 1789 |
+
if st.button("إغلاق" if st.session_state.language == 'ar' else "Close"):
|
| 1790 |
+
st.session_state.show_export_modal = False
|
| 1791 |
+
st.rerun()
|
| 1792 |
+
|
| 1793 |
+
# Note: external live slice helper removed to keep the app simple and fully local
|
| 1794 |
+
|
| 1795 |
+
# --- Step 2: Review and Customize (REMOVED) ---
|
| 1796 |
+
# This section was removed as requested by user to simplify the interface
|
| 1797 |
+
# Results are now shown directly in show_processing_results() function
|
| 1798 |
+
|
| 1799 |
+
def reset_session():
|
| 1800 |
+
"""Resets the session state by clearing specific keys and re-initializing."""
|
| 1801 |
+
log_to_browser_console("--- INFO: Resetting session state. ---")
|
| 1802 |
+
keys_to_clear = ['step', 'audio_data', 'transcription_data', 'edited_text', 'video_style', 'new_recording']
|
| 1803 |
+
for key in keys_to_clear:
|
| 1804 |
+
if key in st.session_state:
|
| 1805 |
+
del st.session_state[key]
|
| 1806 |
+
initialize_session_state()
|
| 1807 |
+
|
| 1808 |
+
# --- Entry Point ---
|
| 1809 |
+
if __name__ == "__main__":
|
| 1810 |
+
if check_api_key():
|
| 1811 |
+
initialize_session_state()
|
| 1812 |
+
main()
|
app_config.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Configuration Module for SyncMaster
|
| 3 |
+
إعدادات التطبيق الأساسية
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import logging
|
| 8 |
+
|
| 9 |
+
# Configure logging
|
| 10 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 11 |
+
|
| 12 |
+
class AppConfig:
|
| 13 |
+
"""Configuration class for SyncMaster application"""
|
| 14 |
+
|
| 15 |
+
# Server settings
|
| 16 |
+
STREAMLIT_PORT = int(os.getenv('STREAMLIT_PORT', 5050))
|
| 17 |
+
RECORDER_PORT = int(os.getenv('RECORDER_PORT', 5001))
|
| 18 |
+
|
| 19 |
+
# Development vs Production
|
| 20 |
+
IS_PRODUCTION = os.getenv('SPACE_ID') is not None or os.getenv('RAILWAY_ENVIRONMENT') is not None
|
| 21 |
+
|
| 22 |
+
# Host settings
|
| 23 |
+
if IS_PRODUCTION:
|
| 24 |
+
STREAMLIT_HOST = "0.0.0.0"
|
| 25 |
+
RECORDER_HOST = "0.0.0.0"
|
| 26 |
+
else:
|
| 27 |
+
STREAMLIT_HOST = "localhost"
|
| 28 |
+
RECORDER_HOST = "localhost"
|
| 29 |
+
|
| 30 |
+
# Integration settings
|
| 31 |
+
USE_INTEGRATED_SERVER = IS_PRODUCTION or os.getenv('USE_INTEGRATED_SERVER', 'true').lower() == 'true'
|
| 32 |
+
|
| 33 |
+
# Logging
|
| 34 |
+
LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO')
|
| 35 |
+
|
| 36 |
+
@classmethod
|
| 37 |
+
def get_streamlit_url(cls):
|
| 38 |
+
"""Get the Streamlit application URL"""
|
| 39 |
+
return f"http://{cls.STREAMLIT_HOST}:{cls.STREAMLIT_PORT}"
|
| 40 |
+
|
| 41 |
+
@classmethod
|
| 42 |
+
def get_recorder_url(cls):
|
| 43 |
+
"""Get the recorder server URL"""
|
| 44 |
+
return f"http://{cls.RECORDER_HOST}:{cls.RECORDER_PORT}"
|
| 45 |
+
|
| 46 |
+
@classmethod
|
| 47 |
+
def log_config(cls):
|
| 48 |
+
"""Log current configuration"""
|
| 49 |
+
logging.info("📋 SyncMaster Configuration:")
|
| 50 |
+
logging.info(f" • Production Mode: {cls.IS_PRODUCTION}")
|
| 51 |
+
logging.info(f" • Integrated Server: {cls.USE_INTEGRATED_SERVER}")
|
| 52 |
+
logging.info(f" • Streamlit: {cls.get_streamlit_url()}")
|
| 53 |
+
logging.info(f" • Recorder: {cls.get_recorder_url()}")
|
| 54 |
+
|
| 55 |
+
# Initialize configuration
|
| 56 |
+
config = AppConfig()
|
| 57 |
+
|
| 58 |
+
if __name__ == "__main__":
|
| 59 |
+
config.log_config()
|
app_launcher.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
App Launcher - يشغل التطبيق مع الخادم المدمج
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import sys
|
| 8 |
+
|
| 9 |
+
# Add current directory to path
|
| 10 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 11 |
+
|
| 12 |
+
# Force start integrated server
|
| 13 |
+
print("🚀 Starting integrated recorder server...")
|
| 14 |
+
try:
|
| 15 |
+
from integrated_server import integrated_server
|
| 16 |
+
|
| 17 |
+
# Force start the server
|
| 18 |
+
if not integrated_server.is_running:
|
| 19 |
+
result = integrated_server.start_recorder_server()
|
| 20 |
+
if result:
|
| 21 |
+
print("✅ Recorder server started successfully")
|
| 22 |
+
else:
|
| 23 |
+
print("⚠️ Warning: Could not start recorder server")
|
| 24 |
+
else:
|
| 25 |
+
print("✅ Recorder server already running")
|
| 26 |
+
|
| 27 |
+
except Exception as e:
|
| 28 |
+
print(f"❌ Error starting recorder server: {e}")
|
| 29 |
+
|
| 30 |
+
print("📱 Loading main application...")
|
| 31 |
+
|
| 32 |
+
# Execute the app.py content directly
|
| 33 |
+
if __name__ == "__main__":
|
| 34 |
+
# If running directly, execute app.py
|
| 35 |
+
exec(open('app.py').read())
|
| 36 |
+
else:
|
| 37 |
+
# If imported by Streamlit, import and execute
|
| 38 |
+
try:
|
| 39 |
+
exec(open('app.py').read())
|
| 40 |
+
print("✅ Application loaded successfully")
|
| 41 |
+
except Exception as e:
|
| 42 |
+
print(f"❌ Error loading application: {e}")
|
| 43 |
+
raise
|
audio_processor.py
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# audio_processor.py - Enhanced with AI Translation Support
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
import tempfile
|
| 6 |
+
from typing import List, Dict, Optional, Tuple
|
| 7 |
+
import json
|
| 8 |
+
import traceback
|
| 9 |
+
|
| 10 |
+
# --- DEFINITIVE NUMBA FIX ---
|
| 11 |
+
# This MUST be done BEFORE importing librosa
|
| 12 |
+
os.environ["NUMBA_CACHE_DIR"] = "/tmp"
|
| 13 |
+
|
| 14 |
+
# Now, import librosa safely
|
| 15 |
+
import librosa
|
| 16 |
+
# --- END OF FIX ---
|
| 17 |
+
|
| 18 |
+
import google.generativeai as genai
|
| 19 |
+
from translator import AITranslator
|
| 20 |
+
import requests
|
| 21 |
+
from google.api_core import exceptions as google_exceptions
|
| 22 |
+
|
| 23 |
+
class AudioProcessor:
|
| 24 |
+
def __init__(self):
|
| 25 |
+
self.translator = None
|
| 26 |
+
self.init_error = None
|
| 27 |
+
self._initialize_translator()
|
| 28 |
+
|
| 29 |
+
def _initialize_translator(self):
|
| 30 |
+
"""Initialize AI translator for multi-language support"""
|
| 31 |
+
try:
|
| 32 |
+
self.translator = AITranslator()
|
| 33 |
+
if self.translator.init_error:
|
| 34 |
+
print(f"--- WARNING: Translator has initialization error: {self.translator.init_error} ---")
|
| 35 |
+
except Exception as e:
|
| 36 |
+
print(f"--- WARNING: Translator initialization failed: {str(e)} ---")
|
| 37 |
+
self.translator = None
|
| 38 |
+
|
| 39 |
+
def transcribe_audio(self, audio_file_path: str) -> Tuple[Optional[str], Optional[str], Optional[str]]:
|
| 40 |
+
"""
|
| 41 |
+
Transcribes audio. Returns (text, error_message).
|
| 42 |
+
Uses Gemini first (if available), then falls back to Groq Whisper.
|
| 43 |
+
"""
|
| 44 |
+
if not os.path.exists(audio_file_path):
|
| 45 |
+
return None, f"--- ERROR: Audio file for transcription not found at: {audio_file_path} ---", None
|
| 46 |
+
|
| 47 |
+
# Try Gemini first if available
|
| 48 |
+
gemini_err = None
|
| 49 |
+
try:
|
| 50 |
+
if self.translator and self.translator.model:
|
| 51 |
+
audio_file = genai.upload_file(path=audio_file_path)
|
| 52 |
+
prompt = (
|
| 53 |
+
"You are an ASR system. Transcribe the audio accurately. "
|
| 54 |
+
"Auto-detect the spoken language and return ONLY the verbatim transcript in that same language. "
|
| 55 |
+
"Do not translate. Do not add labels or timestamps."
|
| 56 |
+
)
|
| 57 |
+
response = self.translator.model.generate_content([prompt, audio_file])
|
| 58 |
+
if response and hasattr(response, 'text') and response.text:
|
| 59 |
+
return response.text.strip(), None, "Gemini"
|
| 60 |
+
else:
|
| 61 |
+
gemini_err = "--- WARNING: Gemini returned an empty response for transcription. ---"
|
| 62 |
+
except google_exceptions.ResourceExhausted:
|
| 63 |
+
gemini_err = "--- QUOTA ERROR: You have exceeded the daily free usage limit for the AI service. Please wait for your quota to reset (usually within 24 hours) or upgrade your Google AI plan. ---"
|
| 64 |
+
except Exception:
|
| 65 |
+
gemini_err = f"--- FATAL ERROR during Gemini transcription: {traceback.format_exc()} ---"
|
| 66 |
+
|
| 67 |
+
# Fallback: Groq Whisper
|
| 68 |
+
text, groq_err = self._transcribe_with_groq(audio_file_path)
|
| 69 |
+
if text:
|
| 70 |
+
return text, None, "Groq Whisper"
|
| 71 |
+
|
| 72 |
+
# If all failed
|
| 73 |
+
combined_err = groq_err or gemini_err or "--- ERROR: No transcription provider available. ---"
|
| 74 |
+
return None, combined_err, None
|
| 75 |
+
|
| 76 |
+
def _transcribe_with_groq(self, audio_file_path: str) -> Tuple[Optional[str], Optional[str]]:
|
| 77 |
+
"""Transcribe using Groq Whisper-compatible endpoint. Returns (text, error)."""
|
| 78 |
+
try:
|
| 79 |
+
load_dotenv()
|
| 80 |
+
groq_key = os.getenv("GROQ_API_KEY")
|
| 81 |
+
if not groq_key:
|
| 82 |
+
return None, "--- ERROR: GROQ_API_KEY not set. ---"
|
| 83 |
+
model = os.getenv("GROQ_WHISPER_MODEL", "whisper-large-v3")
|
| 84 |
+
url = "https://api.groq.com/openai/v1/audio/transcriptions"
|
| 85 |
+
headers = {"Authorization": f"Bearer {groq_key}"}
|
| 86 |
+
# Guess mime type by extension
|
| 87 |
+
filename = os.path.basename(audio_file_path)
|
| 88 |
+
mime = "audio/wav"
|
| 89 |
+
if filename.lower().endswith(".mp3"):
|
| 90 |
+
mime = "audio/mpeg"
|
| 91 |
+
elif filename.lower().endswith(".m4a"):
|
| 92 |
+
mime = "audio/mp4"
|
| 93 |
+
data = {
|
| 94 |
+
"model": model,
|
| 95 |
+
"response_format": "json",
|
| 96 |
+
}
|
| 97 |
+
with open(audio_file_path, "rb") as f:
|
| 98 |
+
files = {"file": (filename, f, mime)}
|
| 99 |
+
resp = requests.post(url, headers=headers, files=files, data=data, timeout=60)
|
| 100 |
+
if not resp.ok:
|
| 101 |
+
try:
|
| 102 |
+
err = resp.json()
|
| 103 |
+
except Exception:
|
| 104 |
+
err = {"error": resp.text}
|
| 105 |
+
return None, f"--- ERROR: Groq transcription error {resp.status_code}: {err} ---"
|
| 106 |
+
out = resp.json()
|
| 107 |
+
text = out.get("text")
|
| 108 |
+
if not text:
|
| 109 |
+
return None, "--- ERROR: Groq transcription returned no text. ---"
|
| 110 |
+
return text.strip(), None
|
| 111 |
+
except Exception:
|
| 112 |
+
return None, f"--- FATAL ERROR during Groq transcription: {traceback.format_exc()} ---"
|
| 113 |
+
|
| 114 |
+
def get_audio_duration(self, audio_file_path: str) -> Tuple[Optional[float], Optional[str]]:
|
| 115 |
+
"""
|
| 116 |
+
Gets audio duration. Returns (duration, error_message).
|
| 117 |
+
"""
|
| 118 |
+
try:
|
| 119 |
+
if not os.path.exists(audio_file_path):
|
| 120 |
+
return None, f"--- ERROR: Audio file for duration not found at: {audio_file_path} ---"
|
| 121 |
+
|
| 122 |
+
duration = librosa.get_duration(path=audio_file_path)
|
| 123 |
+
if duration is None or duration < 0.1:
|
| 124 |
+
return None, f"--- ERROR: librosa returned an invalid duration: {duration}s ---"
|
| 125 |
+
return duration, None
|
| 126 |
+
except Exception as e:
|
| 127 |
+
error_msg = f"--- FATAL ERROR getting audio duration with librosa: {traceback.format_exc()} ---"
|
| 128 |
+
return None, error_msg
|
| 129 |
+
|
| 130 |
+
def get_word_timestamps(self, audio_file_path: str) -> Tuple[List[Dict], List[str], Optional[str]]:
|
| 131 |
+
"""
|
| 132 |
+
Generates timestamps. Returns (timestamps, log_messages).
|
| 133 |
+
"""
|
| 134 |
+
logs = ["--- INFO: Starting get_word_timestamps... ---"]
|
| 135 |
+
|
| 136 |
+
transcription, error, model_used = self.transcribe_audio(audio_file_path)
|
| 137 |
+
if error:
|
| 138 |
+
logs.append(error)
|
| 139 |
+
return [], logs, model_used
|
| 140 |
+
logs.append(f"--- DEBUG: Transcription successful. Text: '{transcription[:50]}...'")
|
| 141 |
+
|
| 142 |
+
audio_duration, error = self.get_audio_duration(audio_file_path)
|
| 143 |
+
if error:
|
| 144 |
+
logs.append(error)
|
| 145 |
+
return [], logs, model_used
|
| 146 |
+
logs.append(f"--- DEBUG: Audio duration successful. Duration: {audio_duration:.2f}s")
|
| 147 |
+
|
| 148 |
+
words = transcription.split()
|
| 149 |
+
if not words:
|
| 150 |
+
logs.append("--- WARNING: Transcription resulted in zero words. ---")
|
| 151 |
+
return [], logs, model_used
|
| 152 |
+
|
| 153 |
+
logs.append(f"--- INFO: Distributing {len(words)} words across the duration. ---")
|
| 154 |
+
word_timestamps = []
|
| 155 |
+
total_words = len(words)
|
| 156 |
+
usable_duration = max(0, audio_duration - 1.0)
|
| 157 |
+
|
| 158 |
+
for i, word in enumerate(words):
|
| 159 |
+
start_time = 0.5 + (i * (usable_duration / total_words))
|
| 160 |
+
end_time = 0.5 + ((i + 1) * (usable_duration / total_words))
|
| 161 |
+
word_timestamps.append({'word': word.strip(), 'start': round(start_time, 3), 'end': round(end_time, 3)})
|
| 162 |
+
|
| 163 |
+
logs.append(f"--- SUCCESS: Generated {len(word_timestamps)} word timestamps. ---")
|
| 164 |
+
return word_timestamps, logs, model_used
|
| 165 |
+
|
| 166 |
+
def get_word_timestamps_with_translation(self, audio_file_path: str, target_language: str = 'ar') -> Tuple[Dict, List[str]]:
|
| 167 |
+
"""
|
| 168 |
+
Enhanced function that provides both transcription and translation
|
| 169 |
+
|
| 170 |
+
Args:
|
| 171 |
+
audio_file_path: Path to audio file
|
| 172 |
+
target_language: Target language for translation ('ar' for Arabic)
|
| 173 |
+
|
| 174 |
+
Returns:
|
| 175 |
+
Tuple of (result_dict, log_messages)
|
| 176 |
+
result_dict contains: {
|
| 177 |
+
'original_text': str,
|
| 178 |
+
'translated_text': str,
|
| 179 |
+
'word_timestamps': List[Dict],
|
| 180 |
+
'translated_timestamps': List[Dict],
|
| 181 |
+
'language_detected': str,
|
| 182 |
+
'target_language': str
|
| 183 |
+
}
|
| 184 |
+
"""
|
| 185 |
+
logs = ["--- INFO: Starting enhanced transcription with translation... ---"]
|
| 186 |
+
|
| 187 |
+
# Get original transcription and timestamps
|
| 188 |
+
word_timestamps, transcription_logs, model_used = self.get_word_timestamps(audio_file_path)
|
| 189 |
+
logs.extend(transcription_logs)
|
| 190 |
+
|
| 191 |
+
if not word_timestamps:
|
| 192 |
+
# Fallback: try plain transcription (Gemini → Groq) then synthesize timestamps
|
| 193 |
+
logs.append("--- INFO: Falling back to plain transcription because timestamps are empty. ---")
|
| 194 |
+
plain_text, err, model_used_fallback = self.transcribe_audio(audio_file_path)
|
| 195 |
+
if model_used_fallback:
|
| 196 |
+
model_used = model_used_fallback
|
| 197 |
+
if not plain_text:
|
| 198 |
+
logs.append(err or "--- ERROR: Plain transcription fallback failed ---")
|
| 199 |
+
return {}, logs
|
| 200 |
+
logs.append(f"--- SUCCESS: Plain transcription fallback succeeded. Model: {model_used}")
|
| 201 |
+
# Synthesize naive word-level timestamps across duration
|
| 202 |
+
try:
|
| 203 |
+
duration, derr = self.get_audio_duration(audio_file_path)
|
| 204 |
+
if derr:
|
| 205 |
+
logs.append(derr)
|
| 206 |
+
duration = 0.0
|
| 207 |
+
words = plain_text.split()
|
| 208 |
+
if not words:
|
| 209 |
+
logs.append("--- WARNING: Fallback transcription produced zero words. ---")
|
| 210 |
+
return {}, logs
|
| 211 |
+
if duration and duration > 0.1:
|
| 212 |
+
usable_duration = max(0, duration - 1.0)
|
| 213 |
+
start_offset = 0.5
|
| 214 |
+
else:
|
| 215 |
+
# If duration not available, assume ~0.4s per word
|
| 216 |
+
usable_duration = 0.4 * max(1, len(words))
|
| 217 |
+
start_offset = 0.0
|
| 218 |
+
word_timestamps = []
|
| 219 |
+
total_words = len(words)
|
| 220 |
+
for i, w in enumerate(words):
|
| 221 |
+
start_time = start_offset + (i * (usable_duration / total_words))
|
| 222 |
+
end_time = start_offset + ((i + 1) * (usable_duration / total_words))
|
| 223 |
+
word_timestamps.append({'word': w.strip(), 'start': round(start_time, 3), 'end': round(end_time, 3)})
|
| 224 |
+
logs.append(f"--- INFO: Synthesized {len(word_timestamps)} timestamps from fallback transcript. ---")
|
| 225 |
+
except Exception:
|
| 226 |
+
logs.append(f"--- FATAL ERROR synthesizing timestamps: {traceback.format_exc()} ---")
|
| 227 |
+
return {}, logs
|
| 228 |
+
|
| 229 |
+
# Extract original text
|
| 230 |
+
original_text = " ".join([d['word'] for d in word_timestamps])
|
| 231 |
+
logs.append(f"--- INFO: Original transcription: '{original_text[:50]}...' ---")
|
| 232 |
+
|
| 233 |
+
# Initialize result dictionary
|
| 234 |
+
result = {
|
| 235 |
+
'original_text': original_text,
|
| 236 |
+
'translated_text': '',
|
| 237 |
+
'word_timestamps': word_timestamps,
|
| 238 |
+
'translated_timestamps': [],
|
| 239 |
+
'language_detected': 'unknown',
|
| 240 |
+
'target_language': target_language,
|
| 241 |
+
'translation_success': False,
|
| 242 |
+
'transcription_model': model_used
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
# Check if translator is available
|
| 246 |
+
if not self.translator:
|
| 247 |
+
logs.append("--- WARNING: Translator not available, returning original text only ---")
|
| 248 |
+
result['translated_text'] = original_text
|
| 249 |
+
return result, logs
|
| 250 |
+
|
| 251 |
+
try:
|
| 252 |
+
# Translate the text
|
| 253 |
+
translated_text, translation_error = self.translator.translate_text(
|
| 254 |
+
original_text,
|
| 255 |
+
target_language=target_language
|
| 256 |
+
)
|
| 257 |
+
|
| 258 |
+
if translated_text:
|
| 259 |
+
result['translated_text'] = translated_text
|
| 260 |
+
result['translation_success'] = True
|
| 261 |
+
logs.append(f"--- SUCCESS: Translation completed: '{translated_text[:50]}...' ---")
|
| 262 |
+
|
| 263 |
+
# Create translated timestamps by mapping words
|
| 264 |
+
translated_timestamps = self._create_translated_timestamps(
|
| 265 |
+
word_timestamps,
|
| 266 |
+
original_text,
|
| 267 |
+
translated_text
|
| 268 |
+
)
|
| 269 |
+
result['translated_timestamps'] = translated_timestamps
|
| 270 |
+
logs.append(f"--- INFO: Created {len(translated_timestamps)} translated timestamps ---")
|
| 271 |
+
|
| 272 |
+
else:
|
| 273 |
+
logs.append(f"--- ERROR: Translation failed: {translation_error} ---")
|
| 274 |
+
result['translated_text'] = original_text # Fallback to original
|
| 275 |
+
result['translated_timestamps'] = word_timestamps # Use original timestamps
|
| 276 |
+
|
| 277 |
+
except Exception as e:
|
| 278 |
+
error_msg = f"--- FATAL ERROR during translation process: {traceback.format_exc()} ---"
|
| 279 |
+
logs.append(error_msg)
|
| 280 |
+
result['translated_text'] = original_text # Fallback
|
| 281 |
+
result['translated_timestamps'] = word_timestamps
|
| 282 |
+
|
| 283 |
+
return result, logs
|
| 284 |
+
|
| 285 |
+
def _create_translated_timestamps(self, original_timestamps: List[Dict], original_text: str, translated_text: str) -> List[Dict]:
|
| 286 |
+
"""
|
| 287 |
+
Create timestamps for translated text by proportional mapping
|
| 288 |
+
|
| 289 |
+
Args:
|
| 290 |
+
original_timestamps: Original word timestamps
|
| 291 |
+
original_text: Original transcribed text
|
| 292 |
+
translated_text: Translated text
|
| 293 |
+
|
| 294 |
+
Returns:
|
| 295 |
+
List of translated word timestamps
|
| 296 |
+
"""
|
| 297 |
+
try:
|
| 298 |
+
translated_words = translated_text.split()
|
| 299 |
+
if not translated_words:
|
| 300 |
+
return []
|
| 301 |
+
|
| 302 |
+
# Get total duration from original timestamps
|
| 303 |
+
if not original_timestamps:
|
| 304 |
+
return []
|
| 305 |
+
|
| 306 |
+
start_time = original_timestamps[0]['start']
|
| 307 |
+
end_time = original_timestamps[-1]['end']
|
| 308 |
+
total_duration = end_time - start_time
|
| 309 |
+
|
| 310 |
+
# Create proportional timestamps for translated words
|
| 311 |
+
translated_timestamps = []
|
| 312 |
+
word_count = len(translated_words)
|
| 313 |
+
|
| 314 |
+
for i, word in enumerate(translated_words):
|
| 315 |
+
# Calculate proportional timing
|
| 316 |
+
word_start = start_time + (i * total_duration / word_count)
|
| 317 |
+
word_end = start_time + ((i + 1) * total_duration / word_count)
|
| 318 |
+
|
| 319 |
+
translated_timestamps.append({
|
| 320 |
+
'word': word.strip(),
|
| 321 |
+
'start': round(word_start, 3),
|
| 322 |
+
'end': round(word_end, 3)
|
| 323 |
+
})
|
| 324 |
+
|
| 325 |
+
return translated_timestamps
|
| 326 |
+
|
| 327 |
+
except Exception as e:
|
| 328 |
+
print(f"--- ERROR creating translated timestamps: {str(e)} ---")
|
| 329 |
+
return []
|
| 330 |
+
|
| 331 |
+
def batch_translate_transcription(self, audio_file_path: str, target_languages: List[str]) -> Tuple[Dict, List[str]]:
|
| 332 |
+
"""
|
| 333 |
+
Transcribe audio and translate to multiple languages
|
| 334 |
+
|
| 335 |
+
Args:
|
| 336 |
+
audio_file_path: Path to audio file
|
| 337 |
+
target_languages: List of target language codes
|
| 338 |
+
|
| 339 |
+
Returns:
|
| 340 |
+
Tuple of (results_dict, log_messages)
|
| 341 |
+
"""
|
| 342 |
+
logs = ["--- INFO: Starting batch translation process... ---"]
|
| 343 |
+
|
| 344 |
+
# Get original transcription
|
| 345 |
+
word_timestamps, transcription_logs = self.get_word_timestamps(audio_file_path)
|
| 346 |
+
logs.extend(transcription_logs)
|
| 347 |
+
|
| 348 |
+
if not word_timestamps:
|
| 349 |
+
return {}, logs
|
| 350 |
+
|
| 351 |
+
original_text = " ".join([d['word'] for d in word_timestamps])
|
| 352 |
+
|
| 353 |
+
# Initialize results
|
| 354 |
+
results = {
|
| 355 |
+
'original': {
|
| 356 |
+
'text': original_text,
|
| 357 |
+
'timestamps': word_timestamps,
|
| 358 |
+
'language': 'detected'
|
| 359 |
+
},
|
| 360 |
+
'translations': {}
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
# Translate to each target language
|
| 364 |
+
if self.translator:
|
| 365 |
+
for lang_code in target_languages:
|
| 366 |
+
try:
|
| 367 |
+
translated_text, error = self.translator.translate_text(original_text, lang_code)
|
| 368 |
+
if translated_text:
|
| 369 |
+
translated_timestamps = self._create_translated_timestamps(
|
| 370 |
+
word_timestamps, original_text, translated_text
|
| 371 |
+
)
|
| 372 |
+
results['translations'][lang_code] = {
|
| 373 |
+
'text': translated_text,
|
| 374 |
+
'timestamps': translated_timestamps,
|
| 375 |
+
'success': True
|
| 376 |
+
}
|
| 377 |
+
logs.append(f"--- SUCCESS: Translation to {lang_code} completed ---")
|
| 378 |
+
else:
|
| 379 |
+
results['translations'][lang_code] = {
|
| 380 |
+
'text': original_text,
|
| 381 |
+
'timestamps': word_timestamps,
|
| 382 |
+
'success': False,
|
| 383 |
+
'error': error
|
| 384 |
+
}
|
| 385 |
+
logs.append(f"--- ERROR: Translation to {lang_code} failed: {error} ---")
|
| 386 |
+
except Exception as e:
|
| 387 |
+
logs.append(f"--- FATAL ERROR translating to {lang_code}: {str(e)} ---")
|
| 388 |
+
else:
|
| 389 |
+
logs.append("--- WARNING: Translator not available for batch translation ---")
|
| 390 |
+
|
| 391 |
+
return results, logs
|
check_credentials.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# check_credentials.py - أداة فحص ملف بيانات الاعتماد
|
| 3 |
+
|
| 4 |
+
import os
|
| 5 |
+
import json
|
| 6 |
+
|
| 7 |
+
def check_credentials():
|
| 8 |
+
"""فحص ملف credentials.json والتأكد من صحته"""
|
| 9 |
+
|
| 10 |
+
print("🔍 فحص ملف بيانات الاعتماد...")
|
| 11 |
+
print("=" * 50)
|
| 12 |
+
|
| 13 |
+
# فحص وجود الملف
|
| 14 |
+
if not os.path.exists('credentials.json'):
|
| 15 |
+
print("❌ ملف credentials.json غير موجود!")
|
| 16 |
+
print("💡 تأكد من وضع الملف في نفس مجلد app.py")
|
| 17 |
+
return False
|
| 18 |
+
|
| 19 |
+
print("✅ ملف credentials.json موجود")
|
| 20 |
+
|
| 21 |
+
# فحص محتوى الملف
|
| 22 |
+
try:
|
| 23 |
+
with open('credentials.json', 'r') as f:
|
| 24 |
+
creds = json.load(f)
|
| 25 |
+
|
| 26 |
+
print("✅ الملف يحتوي على JSON صحيح")
|
| 27 |
+
|
| 28 |
+
# فحص البنية
|
| 29 |
+
if 'installed' not in creds:
|
| 30 |
+
print("❌ الملف لا يحتوي على قسم 'installed'")
|
| 31 |
+
return False
|
| 32 |
+
|
| 33 |
+
installed = creds['installed']
|
| 34 |
+
|
| 35 |
+
# فحص الحقول المطلوبة
|
| 36 |
+
required_fields = ['client_id', 'client_secret', 'auth_uri', 'token_uri']
|
| 37 |
+
missing_fields = []
|
| 38 |
+
|
| 39 |
+
for field in required_fields:
|
| 40 |
+
if field not in installed:
|
| 41 |
+
missing_fields.append(field)
|
| 42 |
+
|
| 43 |
+
if missing_fields:
|
| 44 |
+
print(f"❌ الحقول المفقودة: {', '.join(missing_fields)}")
|
| 45 |
+
return False
|
| 46 |
+
|
| 47 |
+
# فحص إذا كانت البيانات وهمية
|
| 48 |
+
client_id = installed.get('client_id', '')
|
| 49 |
+
client_secret = installed.get('client_secret', '')
|
| 50 |
+
|
| 51 |
+
if client_id.startswith('YOUR_CLIENT_ID'):
|
| 52 |
+
print("❌ client_id لا يزال يحتوي على القيمة الافتراضية!")
|
| 53 |
+
print("💡 يجب استبدال الملف بملف حقيقي من Google Cloud Console")
|
| 54 |
+
return False
|
| 55 |
+
|
| 56 |
+
if client_secret.startswith('YOUR_CLIENT_SECRET'):
|
| 57 |
+
print("❌ client_secret لا يزال يحتوي على القيمة الافتراضية!")
|
| 58 |
+
print("💡 يجب استبدال الملف بملف حقيقي من Google Cloud Console")
|
| 59 |
+
return False
|
| 60 |
+
|
| 61 |
+
print("✅ جميع الحقول المطلوبة موجودة")
|
| 62 |
+
print(f"✅ Client ID: {client_id[:20]}...")
|
| 63 |
+
print(f"✅ Project ID: {installed.get('project_id', 'غير محدد')}")
|
| 64 |
+
|
| 65 |
+
return True
|
| 66 |
+
|
| 67 |
+
except json.JSONDecodeError:
|
| 68 |
+
print("❌ الملف لا يحتوي على JSON صحيح!")
|
| 69 |
+
return False
|
| 70 |
+
except Exception as e:
|
| 71 |
+
print(f"❌ خطأ في قراءة الملف: {e}")
|
| 72 |
+
return False
|
| 73 |
+
|
| 74 |
+
def check_token():
|
| 75 |
+
"""فحص ملف token.json إذا كان موجوداً"""
|
| 76 |
+
|
| 77 |
+
print("\n🔍 فحص ملف المصادقة...")
|
| 78 |
+
print("=" * 50)
|
| 79 |
+
|
| 80 |
+
if os.path.exists('token.json'):
|
| 81 |
+
print("✅ ملف token.json موجود")
|
| 82 |
+
try:
|
| 83 |
+
with open('token.json', 'r') as f:
|
| 84 |
+
token = json.load(f)
|
| 85 |
+
|
| 86 |
+
if 'token' in token:
|
| 87 |
+
print("✅ يحتوي على رمز مصادقة")
|
| 88 |
+
|
| 89 |
+
if 'refresh_token' in token:
|
| 90 |
+
print("✅ يحتوي على رمز التحديث")
|
| 91 |
+
|
| 92 |
+
if 'expiry' in token:
|
| 93 |
+
print(f"⏰ تاريخ انتهاء الصلاحية: {token['expiry']}")
|
| 94 |
+
|
| 95 |
+
except Exception as e:
|
| 96 |
+
print(f"⚠️ مشكلة في ملف token.json: {e}")
|
| 97 |
+
print("💡 يمكنك حذف الملف وإعادة المصادقة")
|
| 98 |
+
else:
|
| 99 |
+
print("ℹ️ ملف token.json غير موجود (طبيعي في أول استخدام)")
|
| 100 |
+
|
| 101 |
+
def main():
|
| 102 |
+
print("🚀 أداة فحص بيانات الاعتماد لـ Google Docs")
|
| 103 |
+
print("=" * 60)
|
| 104 |
+
|
| 105 |
+
creds_ok = check_credentials()
|
| 106 |
+
check_token()
|
| 107 |
+
|
| 108 |
+
print("\n" + "=" * 60)
|
| 109 |
+
|
| 110 |
+
if creds_ok:
|
| 111 |
+
print("🎉 ملف بيانات الاعتماد صحيح!")
|
| 112 |
+
print("💡 يمكنك الآن استخدام زر التصدير")
|
| 113 |
+
else:
|
| 114 |
+
print("❌ يجب إصلاح ملف بيانات الاعتماد أولاً")
|
| 115 |
+
print("📋 راجع ملف GOOGLE_SETUP_SIMPLE.md للحصول على التعليمات")
|
| 116 |
+
|
| 117 |
+
if __name__ == "__main__":
|
| 118 |
+
main()
|
comprehensive_test.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
اختبار شامل للتحقق من إصلاح جميع المشاكل
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import requests
|
| 8 |
+
import json
|
| 9 |
+
import time
|
| 10 |
+
|
| 11 |
+
def test_server_health():
|
| 12 |
+
"""اختبار صحة الخادم"""
|
| 13 |
+
print("🏥 اختبار صحة الخادم...")
|
| 14 |
+
|
| 15 |
+
try:
|
| 16 |
+
response = requests.get('http://localhost:5001/record', timeout=5)
|
| 17 |
+
if response.status_code == 200:
|
| 18 |
+
data = response.json()
|
| 19 |
+
print(f"✅ الخادم يعمل: {data.get('message')}")
|
| 20 |
+
return True
|
| 21 |
+
else:
|
| 22 |
+
print(f"❌ مشكلة في الخادم: {response.status_code}")
|
| 23 |
+
return False
|
| 24 |
+
except Exception as e:
|
| 25 |
+
print(f"❌ لا يمكن الوصول للخادم: {e}")
|
| 26 |
+
return False
|
| 27 |
+
|
| 28 |
+
def test_cors_headers():
|
| 29 |
+
"""اختبار CORS headers"""
|
| 30 |
+
print("\n🔧 اختبار CORS headers...")
|
| 31 |
+
|
| 32 |
+
try:
|
| 33 |
+
# اختبار OPTIONS request
|
| 34 |
+
response = requests.options('http://localhost:5001/summarize', timeout=5)
|
| 35 |
+
|
| 36 |
+
print(f"Status Code: {response.status_code}")
|
| 37 |
+
|
| 38 |
+
# فحص CORS headers
|
| 39 |
+
cors_origin = response.headers.get('Access-Control-Allow-Origin')
|
| 40 |
+
cors_methods = response.headers.get('Access-Control-Allow-Methods')
|
| 41 |
+
cors_headers = response.headers.get('Access-Control-Allow-Headers')
|
| 42 |
+
|
| 43 |
+
print(f"CORS Origin: '{cors_origin}'")
|
| 44 |
+
print(f"CORS Methods: '{cors_methods}'")
|
| 45 |
+
print(f"CORS Headers: '{cors_headers}'")
|
| 46 |
+
|
| 47 |
+
# التحقق من عدم وجود قيم مكررة
|
| 48 |
+
if cors_origin and ',' in cors_origin and cors_origin.count('*') > 1:
|
| 49 |
+
print("❌ مشكلة: CORS Origin يحتوي على قيم مكررة!")
|
| 50 |
+
return False
|
| 51 |
+
elif cors_origin == '*':
|
| 52 |
+
print("✅ CORS Origin صحيح")
|
| 53 |
+
return True
|
| 54 |
+
else:
|
| 55 |
+
print(f"⚠️ CORS Origin غير متوقع: {cors_origin}")
|
| 56 |
+
return False
|
| 57 |
+
|
| 58 |
+
except Exception as e:
|
| 59 |
+
print(f"❌ خطأ في اختبار CORS: {e}")
|
| 60 |
+
return False
|
| 61 |
+
|
| 62 |
+
def test_summarization():
|
| 63 |
+
"""اختبار وظيفة التلخيص"""
|
| 64 |
+
print("\n🤖 اختبار وظيفة التلخيص...")
|
| 65 |
+
|
| 66 |
+
test_data = {
|
| 67 |
+
"text": "Hello, how are you? What are you doing today? Tell me about your work and your plans.",
|
| 68 |
+
"language": "arabic",
|
| 69 |
+
"type": "full"
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
try:
|
| 73 |
+
response = requests.post(
|
| 74 |
+
'http://localhost:5001/summarize',
|
| 75 |
+
json=test_data,
|
| 76 |
+
headers={'Content-Type': 'application/json'},
|
| 77 |
+
timeout=30
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
print(f"Status Code: {response.status_code}")
|
| 81 |
+
|
| 82 |
+
if response.status_code == 200:
|
| 83 |
+
data = response.json()
|
| 84 |
+
if data.get('success'):
|
| 85 |
+
print("✅ التلخيص نجح!")
|
| 86 |
+
summary = data.get('summary', '')
|
| 87 |
+
print(f"الملخص: {summary[:100]}...")
|
| 88 |
+
return True
|
| 89 |
+
else:
|
| 90 |
+
print(f"❌ فشل التلخيص: {data.get('error')}")
|
| 91 |
+
return False
|
| 92 |
+
else:
|
| 93 |
+
print(f"❌ خطأ HTTP: {response.status_code}")
|
| 94 |
+
print(f"الرد: {response.text}")
|
| 95 |
+
return False
|
| 96 |
+
|
| 97 |
+
except Exception as e:
|
| 98 |
+
print(f"❌ خطأ في اختبار التلخيص: {e}")
|
| 99 |
+
return False
|
| 100 |
+
|
| 101 |
+
def test_javascript_syntax():
|
| 102 |
+
"""اختبار صيغة JavaScript"""
|
| 103 |
+
print("\n📝 اختبار صيغة JavaScript...")
|
| 104 |
+
|
| 105 |
+
try:
|
| 106 |
+
with open('templates/recorder.html', 'r', encoding='utf-8') as f:
|
| 107 |
+
content = f.read()
|
| 108 |
+
|
| 109 |
+
# فحص بسيط للأقواس
|
| 110 |
+
js_start = content.find('<script>')
|
| 111 |
+
js_end = content.find('</script>')
|
| 112 |
+
|
| 113 |
+
if js_start == -1 or js_end == -1:
|
| 114 |
+
print("❌ لا يمكن العثور على JavaScript")
|
| 115 |
+
return False
|
| 116 |
+
|
| 117 |
+
js_content = content[js_start:js_end]
|
| 118 |
+
|
| 119 |
+
# عد الأقواس
|
| 120 |
+
open_braces = js_content.count('{')
|
| 121 |
+
close_braces = js_content.count('}')
|
| 122 |
+
|
| 123 |
+
print(f"أقواس فتح: {open_braces}")
|
| 124 |
+
print(f"أقواس إغلاق: {close_braces}")
|
| 125 |
+
|
| 126 |
+
if open_braces == close_braces:
|
| 127 |
+
print("✅ الأقواس متوازنة")
|
| 128 |
+
|
| 129 |
+
# فحص للكلمات المفتاحية الأساسية
|
| 130 |
+
if 'function' in js_content and 'async function' in js_content:
|
| 131 |
+
print("✅ الدوال موجودة")
|
| 132 |
+
return True
|
| 133 |
+
else:
|
| 134 |
+
print("⚠️ لا يمكن العثور على الدوال")
|
| 135 |
+
return False
|
| 136 |
+
else:
|
| 137 |
+
print(f"❌ الأقواس غير متوازنة! الفرق: {open_braces - close_braces}")
|
| 138 |
+
return False
|
| 139 |
+
|
| 140 |
+
except Exception as e:
|
| 141 |
+
print(f"❌ خطأ في فحص JavaScript: {e}")
|
| 142 |
+
return False
|
| 143 |
+
|
| 144 |
+
def test_translation_endpoints():
|
| 145 |
+
"""اختبار endpoints الترجمة"""
|
| 146 |
+
print("\n🌐 اختبار endpoints الترجمة...")
|
| 147 |
+
|
| 148 |
+
try:
|
| 149 |
+
# اختبار قائمة اللغات
|
| 150 |
+
response = requests.get('http://localhost:5001/languages', timeout=5)
|
| 151 |
+
if response.status_code == 200:
|
| 152 |
+
print("✅ endpoint اللغات يعمل")
|
| 153 |
+
else:
|
| 154 |
+
print(f"⚠️ مشكلة في endpoint اللغات: {response.status_code}")
|
| 155 |
+
|
| 156 |
+
# اختبار UI translations
|
| 157 |
+
response = requests.get('http://localhost:5001/ui-translations/en', timeout=5)
|
| 158 |
+
if response.status_code == 200:
|
| 159 |
+
print("✅ endpoint UI translations يعمل")
|
| 160 |
+
else:
|
| 161 |
+
print(f"⚠️ مشكلة في endpoint UI translations: {response.status_code}")
|
| 162 |
+
|
| 163 |
+
return True
|
| 164 |
+
|
| 165 |
+
except Exception as e:
|
| 166 |
+
print(f"❌ خطأ في اختبار endpoints الترجمة: {e}")
|
| 167 |
+
return False
|
| 168 |
+
|
| 169 |
+
def comprehensive_test():
|
| 170 |
+
"""اختبار شامل لجميع الوظائف"""
|
| 171 |
+
print("🚀 بدء الاختبار الشامل")
|
| 172 |
+
print("=" * 60)
|
| 173 |
+
|
| 174 |
+
tests = [
|
| 175 |
+
("صحة الخادم", test_server_health),
|
| 176 |
+
("CORS Headers", test_cors_headers),
|
| 177 |
+
("وظيفة التلخيص", test_summarization),
|
| 178 |
+
("صيغة JavaScript", test_javascript_syntax),
|
| 179 |
+
("endpoints الترجمة", test_translation_endpoints)
|
| 180 |
+
]
|
| 181 |
+
|
| 182 |
+
results = []
|
| 183 |
+
|
| 184 |
+
for test_name, test_func in tests:
|
| 185 |
+
print(f"\n🧪 اختبار: {test_name}")
|
| 186 |
+
print("-" * 40)
|
| 187 |
+
|
| 188 |
+
try:
|
| 189 |
+
result = test_func()
|
| 190 |
+
results.append((test_name, result))
|
| 191 |
+
|
| 192 |
+
if result:
|
| 193 |
+
print(f"✅ {test_name}: نجح")
|
| 194 |
+
else:
|
| 195 |
+
print(f"❌ {test_name}: فشل")
|
| 196 |
+
|
| 197 |
+
except Exception as e:
|
| 198 |
+
print(f"❌ {test_name}: خطأ - {e}")
|
| 199 |
+
results.append((test_name, False))
|
| 200 |
+
|
| 201 |
+
# النتائج النهائية
|
| 202 |
+
print("\n" + "=" * 60)
|
| 203 |
+
print("📊 ملخص نتائج الاختبار:")
|
| 204 |
+
print("=" * 60)
|
| 205 |
+
|
| 206 |
+
passed = 0
|
| 207 |
+
total = len(results)
|
| 208 |
+
|
| 209 |
+
for test_name, result in results:
|
| 210 |
+
status = "✅ نجح" if result else "❌ فشل"
|
| 211 |
+
print(f" {test_name}: {status}")
|
| 212 |
+
if result:
|
| 213 |
+
passed += 1
|
| 214 |
+
|
| 215 |
+
print(f"\nالنتيجة النهائية: {passed}/{total} اختبارات نجحت")
|
| 216 |
+
|
| 217 |
+
if passed == total:
|
| 218 |
+
print("🎉 جميع الاختبارات نجحت! النظام يعمل بشكل مثالي")
|
| 219 |
+
return True
|
| 220 |
+
else:
|
| 221 |
+
print(f"⚠️ {total - passed} اختبارات فشلت - هناك مشاكل تحتاج إصلاح")
|
| 222 |
+
return False
|
| 223 |
+
|
| 224 |
+
if __name__ == "__main__":
|
| 225 |
+
comprehensive_test()
|
credentials.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"web": {
|
| 3 |
+
"client_id": "739771741359-gk2mkvimn063a2msd6hmkkksn17iamre.apps.googleusercontent.com",
|
| 4 |
+
"project_id": "syncmaster-export",
|
| 5 |
+
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
| 6 |
+
"token_uri": "https://oauth2.googleapis.com/token",
|
| 7 |
+
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
| 8 |
+
"client_secret": "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",
|
| 9 |
+
"redirect_uris": [
|
| 10 |
+
"http://localhost:8502",
|
| 11 |
+
"http://localhost:8502/",
|
| 12 |
+
"http://127.0.0.1:8502",
|
| 13 |
+
"http://127.0.0.1:8502/"
|
| 14 |
+
]
|
| 15 |
+
}
|
| 16 |
+
}
|
custom_components/st-audiorec/.streamlit/config.toml
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8eec7cce049f088766524596c7dd6229756df0d6331c8cfab099df7d2ebc9d5d
|
| 3 |
+
size 662
|
custom_components/st-audiorec/LICENCE
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:784d3a6fdb08d429f5de43125e9962e780d34cdf9b5f14b681c9a2d8e905bfec
|
| 3 |
+
size 1080
|
custom_components/st-audiorec/README.md
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:54716e3eaf4e6047fb50d99176bd2c6b24124b978af2eb30e44a8c6a74cdb9c0
|
| 3 |
+
size 1993
|
custom_components/st-audiorec/demo.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:efbe93ef9297821f3a1e1f1bdf0f75fb4a4351b154439d01d9ea0cbd49b996b8
|
| 3 |
+
size 2430
|
custom_components/st-audiorec/setup.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:492fd555416f2807df31dc43e10b9d3cd8d5c18636c586c7f37988e1d8b854c1
|
| 3 |
+
size 786
|
custom_components/st-audiorec/st_audiorec/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3f714847f1f4a2490e5dd80bc1eeb7b6fcb7850a3e682b491a28dd30fead486c
|
| 3 |
+
size 1622
|
custom_components/st-audiorec/st_audiorec/frontend/.prettierrc
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3375a44313ae0b6753868a7ae00dc03f618b0c23785b15980482e6b9457ca0f8
|
| 3 |
+
size 72
|
custom_components/st-audiorec/st_audiorec/frontend/build/asset-manifest.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d923ea03d2475f02335299c4a15a1ca84291e9cbbcd558e6abdb318abe5ccc6f
|
| 3 |
+
size 859
|
custom_components/st-audiorec/st_audiorec/frontend/build/bootstrap.min.css
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f396767523d7b7ce621d90aae93cbbd7a516275898efd19020be38aa5ae85d5c
|
| 3 |
+
size 206913
|
custom_components/st-audiorec/st_audiorec/frontend/build/index.html
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e007dc7fb036886292b996d076d57c6eb6eedf32b8e2c5489ad9f6d29d59f088
|
| 3 |
+
size 2175
|
custom_components/st-audiorec/st_audiorec/frontend/build/precache-manifest.30096e2fd9f149157a833e729e772f72.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e318206d88822468d480fd4047df1439dd2dadb500d846636314b39afabb8af4
|
| 3 |
+
size 564
|
custom_components/st-audiorec/st_audiorec/frontend/build/service-worker.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8243bfeb139d7acdf475a748daa48068cde6e5d62a4c2242326e3d6bbfbc6d78
|
| 3 |
+
size 1183
|
custom_components/st-audiorec/st_audiorec/frontend/build/static/js/2.ca2bba73.chunk.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8810a6d23c8292fa11953f5c2c762e6bd658f11316f12879d0a6d4e05f7df5a1
|
| 3 |
+
size 465885
|
custom_components/st-audiorec/st_audiorec/frontend/build/static/js/2.ca2bba73.chunk.js.LICENSE.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:83bbf722e5b20cfb2920ac1c234ffa5ccde3baa9d8d5a87b4cc90f81ef649a47
|
| 3 |
+
size 1653
|
custom_components/st-audiorec/st_audiorec/frontend/build/static/js/2.ca2bba73.chunk.js.map
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:25cecefabe1287205f5f99bfd33159566c8d97bc56dadba39d70fcaf160c7998
|
| 3 |
+
size 1634044
|
custom_components/st-audiorec/st_audiorec/frontend/build/static/js/main.85742990.chunk.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:28cf7e640dbe5e67fdde3e5e4eea1d9053901a0612be430efdd2968509feb279
|
| 3 |
+
size 13457
|
custom_components/st-audiorec/st_audiorec/frontend/build/static/js/main.85742990.chunk.js.map
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b9c4877643f7494e0fea5996bc57d8667c1258cb58311d1d530a957303ffd698
|
| 3 |
+
size 38454
|