Yeetek commited on
Commit
b3e0a65
·
verified ·
1 Parent(s): 069d845

Upload 17 files

Browse files
Files changed (8) hide show
  1. Dockerfile +6 -3
  2. README.md +8 -8
  3. config/settings.py +1 -1
  4. gradio_app.py +381 -0
  5. models/input.py +4 -2
  6. models/output.py +6 -3
  7. requirements.txt +5 -1
  8. start.sh +31 -0
Dockerfile CHANGED
@@ -31,17 +31,20 @@ RUN pip install --no-cache-dir -r requirements.txt
31
  # Copy application code
32
  COPY . .
33
 
 
 
 
34
  # Create non-root user for security
35
  RUN useradd --create-home --shell /bin/bash app \
36
  && chown -R app:app /app
37
  USER app
38
 
39
  # Health check
40
- HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
41
- CMD curl -f http://localhost:7860/health || exit 1
42
 
43
  # Expose port (HuggingFace Spaces uses 7860)
44
  EXPOSE 7860
45
 
46
  # Start command optimized for HuggingFace Spaces
47
- CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "1"]
 
31
  # Copy application code
32
  COPY . .
33
 
34
+ # Make startup script executable
35
+ RUN chmod +x start.sh
36
+
37
  # Create non-root user for security
38
  RUN useradd --create-home --shell /bin/bash app \
39
  && chown -R app:app /app
40
  USER app
41
 
42
  # Health check
43
+ HEALTHCHECK --interval=30s --timeout=30s --start-period=15s --retries=3 \
44
+ CMD curl -f http://localhost:7860/ || exit 1
45
 
46
  # Expose port (HuggingFace Spaces uses 7860)
47
  EXPOSE 7860
48
 
49
  # Start command optimized for HuggingFace Spaces
50
+ CMD ["./start.sh"]
README.md CHANGED
@@ -4,7 +4,7 @@ emoji: 🎯
4
  colorFrom: blue
5
  colorTo: purple
6
  sdk: docker
7
- app_file: app.py
8
  pinned: false
9
  license: mit
10
  ---
@@ -20,7 +20,7 @@ A production-ready microservice that uses **Anthropic's Claude models** for inte
20
 
21
  ## 🚀 **Live Demo on HuggingFace Spaces**
22
 
23
- Try the API directly: [https://huggingface.co/spaces/YOUR-USERNAME/anthropic-topic-segmentation](https://huggingface.co/spaces/YOUR-USERNAME/anthropic-topic-segmentation)
24
 
25
  ## ✨ **Key Features**
26
 
@@ -55,7 +55,7 @@ Successfully processes Czech Shoptet integration discussions, extracting:
55
 
56
  ```bash
57
  # Clone the repository
58
- git clone https://huggingface.co/spaces/YOUR-USERNAME/anthropic-topic-segmentation
59
  cd anthropic-topic-segmentation
60
 
61
  # Create .env file
@@ -84,12 +84,12 @@ uvicorn app:app --host 0.0.0.0 --port 7860
84
 
85
  ### **Health Check**
86
  ```bash
87
- curl https://your-space.hf.space/health
88
  ```
89
 
90
  ### **Topic Extraction**
91
  ```bash
92
- curl -X POST https://your-space.hf.space/segment \
93
  -H "Content-Type: application/json" \
94
  -d '{
95
  "sentences": [
@@ -110,8 +110,8 @@ curl -X POST https://your-space.hf.space/segment \
110
  ```
111
 
112
  ### **Interactive Documentation**
113
- - **Swagger UI**: https://your-space.hf.space/docs
114
- - **ReDoc**: https://your-space.hf.space/redoc
115
 
116
  ## 🔧 **n8n Integration**
117
 
@@ -122,7 +122,7 @@ Perfect for workflow automation:
122
  "workflow_name": "Czech E-commerce Analysis",
123
  "http_request": {
124
  "method": "POST",
125
- "url": "https://your-space.hf.space/segment",
126
  "body": {
127
  "sentences": "{{ $json.transcript }}",
128
  "prompt_config": {
 
4
  colorFrom: blue
5
  colorTo: purple
6
  sdk: docker
7
+ app_file: gradio_app.py
8
  pinned: false
9
  license: mit
10
  ---
 
20
 
21
  ## 🚀 **Live Demo on HuggingFace Spaces**
22
 
23
+ Try the API directly: [https://huggingface.co/spaces/Yeetek/anthropic-topic-segmentation](https://huggingface.co/spaces/Yeetek/anthropic-topic-segmentation)
24
 
25
  ## ✨ **Key Features**
26
 
 
55
 
56
  ```bash
57
  # Clone the repository
58
+ git clone https://huggingface.co/spaces/Yeetek/anthropic-topic-segmentation
59
  cd anthropic-topic-segmentation
60
 
61
  # Create .env file
 
84
 
85
  ### **Health Check**
86
  ```bash
87
+ curl https://yeetek-anthropic-topic-segmentation.hf.space/health
88
  ```
89
 
90
  ### **Topic Extraction**
91
  ```bash
92
+ curl -X POST https://yeetek-anthropic-topic-segmentation.hf.space/segment \
93
  -H "Content-Type: application/json" \
94
  -d '{
95
  "sentences": [
 
110
  ```
111
 
112
  ### **Interactive Documentation**
113
+ - **Swagger UI**: https://yeetek-anthropic-topic-segmentation.hf.space/docs
114
+ - **ReDoc**: https://yeetek-anthropic-topic-segmentation.hf.space/redoc
115
 
116
  ## 🔧 **n8n Integration**
117
 
 
122
  "workflow_name": "Czech E-commerce Analysis",
123
  "http_request": {
124
  "method": "POST",
125
+ "url": "https://yeetek-anthropic-topic-segmentation.hf.space/segment",
126
  "body": {
127
  "sentences": "{{ $json.transcript }}",
128
  "prompt_config": {
config/settings.py CHANGED
@@ -25,7 +25,7 @@ class AnthropicModel(str, Enum):
25
  """Supported Anthropic models."""
26
  CLAUDE_3_5_SONNET = "claude-3-5-sonnet-20241022"
27
  CLAUDE_3_5_HAIKU = "claude-3-5-haiku-20241022"
28
- CLAUDE_3_SONNET = "claude-3-sonnet-20240229" # Deprecated but kept for compatibility
29
  CLAUDE_3_HAIKU = "claude-3-haiku-20240307" # Deprecated but kept for compatibility
30
 
31
 
 
25
  """Supported Anthropic models."""
26
  CLAUDE_3_5_SONNET = "claude-3-5-sonnet-20241022"
27
  CLAUDE_3_5_HAIKU = "claude-3-5-haiku-20241022"
28
+ CLAUDE_3_SONNET = "claude-3-5-sonnet-20241022" # Updated to current version
29
  CLAUDE_3_HAIKU = "claude-3-haiku-20240307" # Deprecated but kept for compatibility
30
 
31
 
gradio_app.py ADDED
@@ -0,0 +1,381 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Gradio interface for Anthropic Topic Segmentation Microservice.
3
+
4
+ This creates a web interface that displays the README content as the main page
5
+ and provides an interactive API interface for HuggingFace Spaces.
6
+ """
7
+
8
+ import gradio as gr
9
+ import requests
10
+ import json
11
+ import os
12
+ from typing import Dict, Any, List
13
+ import markdown
14
+
15
+ # Read the README content
16
+ def load_readme():
17
+ """Load and convert README.md to HTML."""
18
+ try:
19
+ with open("README.md", "r", encoding="utf-8") as f:
20
+ readme_content = f.read()
21
+
22
+ # Remove YAML frontmatter
23
+ if readme_content.startswith("---"):
24
+ parts = readme_content.split("---", 2)
25
+ if len(parts) >= 3:
26
+ readme_content = parts[2].strip()
27
+
28
+ # Convert markdown to HTML
29
+ html_content = markdown.markdown(readme_content, extensions=['codehilite', 'fenced_code'])
30
+ return html_content
31
+ except Exception as e:
32
+ return f"<p>Error loading README: {str(e)}</p>"
33
+
34
+ # API endpoint URL (FastAPI backend runs on port 8000)
35
+ API_BASE_URL = "http://localhost:8000"
36
+
37
+ def call_health_check():
38
+ """Call the health check endpoint."""
39
+ try:
40
+ response = requests.get(f"{API_BASE_URL}/health", timeout=10)
41
+ if response.status_code == 200:
42
+ return "✅ API is healthy", json.dumps(response.json(), indent=2)
43
+ else:
44
+ return f"❌ API returned status {response.status_code}", response.text
45
+ except Exception as e:
46
+ return f"❌ Error connecting to API", str(e)
47
+
48
+ def call_segment_api(sentences_json: str, template: str, language: str, business_domain: str):
49
+ """Call the topic segmentation API."""
50
+ try:
51
+ # Parse the input JSON
52
+ try:
53
+ sentences_data = json.loads(sentences_json)
54
+ except json.JSONDecodeError as e:
55
+ return f"❌ Invalid JSON format: {str(e)}", ""
56
+
57
+ # Prepare the request
58
+ request_data = {
59
+ "sentences": sentences_data,
60
+ "prompt_config": {
61
+ "template": template,
62
+ "language": language,
63
+ "business_domain": business_domain if business_domain else None
64
+ }
65
+ }
66
+
67
+ # Make the API call
68
+ response = requests.post(
69
+ f"{API_BASE_URL}/segment",
70
+ json=request_data,
71
+ timeout=120,
72
+ headers={"Content-Type": "application/json"}
73
+ )
74
+
75
+ if response.status_code == 200:
76
+ result = response.json()
77
+ # Format the response nicely
78
+ topics_summary = f"✅ Successfully extracted {len(result.get('topics', []))} topics"
79
+ return topics_summary, json.dumps(result, indent=2)
80
+ else:
81
+ return f"❌ API returned status {response.status_code}", response.text
82
+
83
+ except Exception as e:
84
+ return f"❌ Error calling API: {str(e)}", ""
85
+
86
+ # Load README content
87
+ readme_html = load_readme()
88
+
89
+ # Sample data for the API demo
90
+ sample_sentences = [
91
+ {
92
+ "text": "Zákazníci požadují nestandardní úpravy košíku v Shoptetu.",
93
+ "speaker": "Client",
94
+ "start_time": 2.01,
95
+ "end_time": 8.45,
96
+ "sentence_index": 1
97
+ },
98
+ {
99
+ "text": "Potřebujeme implementovat speciální platební metody.",
100
+ "speaker": "Client",
101
+ "start_time": 8.45,
102
+ "end_time": 15.2,
103
+ "sentence_index": 2
104
+ },
105
+ {
106
+ "text": "API má problémy s rychlostí načítání.",
107
+ "speaker": "Developer",
108
+ "start_time": 15.2,
109
+ "end_time": 20.1,
110
+ "sentence_index": 3
111
+ }
112
+ ]
113
+
114
+ sample_json = json.dumps(sample_sentences, indent=2, ensure_ascii=False)
115
+
116
+ # Create the Gradio interface
117
+ with gr.Blocks(
118
+ title="🎯 Anthropic Topic Segmentation Microservice",
119
+ theme=gr.themes.Soft(),
120
+ css="""
121
+ .main-header {
122
+ text-align: center;
123
+ padding: 20px;
124
+ background: linear-gradient(90deg, #3b82f6, #8b5cf6);
125
+ color: white;
126
+ border-radius: 10px;
127
+ margin-bottom: 20px;
128
+ }
129
+ .api-section {
130
+ border: 2px solid #e5e7eb;
131
+ border-radius: 10px;
132
+ padding: 20px;
133
+ margin: 10px 0;
134
+ }
135
+ """
136
+ ) as app:
137
+
138
+ # Main header
139
+ gr.HTML("""
140
+ <div class="main-header">
141
+ <h1>🎯 Anthropic Topic Segmentation Microservice</h1>
142
+ <p>AI-powered topic extraction from Czech e-commerce transcripts using Anthropic Claude</p>
143
+ <p><strong>✅ Production Ready | 🌍 Multi-Language | 🔄 n8n Compatible</strong></p>
144
+ </div>
145
+ """)
146
+
147
+ with gr.Tabs():
148
+ # Tab 1: Documentation (README)
149
+ with gr.Tab("📚 Documentation"):
150
+ gr.HTML(readme_html)
151
+
152
+ # Tab 2: API Testing Interface
153
+ with gr.Tab("🧪 API Testing"):
154
+ gr.HTML('<div class="api-section">')
155
+ gr.Markdown("## 🔍 Health Check")
156
+
157
+ with gr.Row():
158
+ health_btn = gr.Button("Check API Health", variant="primary")
159
+ health_status = gr.Textbox(label="Status", interactive=False)
160
+
161
+ health_response = gr.Code(label="Health Response", language="json")
162
+
163
+ health_btn.click(
164
+ call_health_check,
165
+ outputs=[health_status, health_response]
166
+ )
167
+
168
+ gr.HTML('</div><div class="api-section">')
169
+ gr.Markdown("## 🎯 Topic Segmentation")
170
+ gr.Markdown("Test the topic extraction API with your own data or use the sample below:")
171
+
172
+ with gr.Row():
173
+ with gr.Column(scale=2):
174
+ sentences_input = gr.Code(
175
+ label="Sentences JSON",
176
+ language="json",
177
+ value=sample_json,
178
+ lines=15
179
+ )
180
+
181
+ with gr.Column(scale=1):
182
+ template_dropdown = gr.Dropdown(
183
+ choices=["interview", "customer_call", "feedback_ticket", "general_commentary"],
184
+ value="customer_call",
185
+ label="Template"
186
+ )
187
+
188
+ language_dropdown = gr.Dropdown(
189
+ choices=["cs", "en", "sk", "auto"],
190
+ value="cs",
191
+ label="Language"
192
+ )
193
+
194
+ business_domain = gr.Textbox(
195
+ label="Business Domain (optional)",
196
+ value="E-commerce",
197
+ placeholder="e.g., E-commerce, Healthcare, Finance"
198
+ )
199
+
200
+ segment_btn = gr.Button("Extract Topics", variant="primary")
201
+
202
+ with gr.Row():
203
+ segment_status = gr.Textbox(label="Status", interactive=False)
204
+
205
+ segment_response = gr.Code(label="API Response", language="json", lines=20)
206
+
207
+ segment_btn.click(
208
+ call_segment_api,
209
+ inputs=[sentences_input, template_dropdown, language_dropdown, business_domain],
210
+ outputs=[segment_status, segment_response]
211
+ )
212
+
213
+ gr.HTML('</div>')
214
+
215
+ # Tab 3: API Documentation
216
+ with gr.Tab("📖 API Reference"):
217
+ gr.Markdown("""
218
+ ## 🔗 API Endpoints
219
+
220
+ ### Base URL
221
+ ```
222
+ https://yeetek-anthropic-topic-segmentation.hf.space
223
+ ```
224
+
225
+ ### Endpoints
226
+
227
+ #### `GET /health`
228
+ Check the health status of the API and Anthropic integration.
229
+
230
+ #### `POST /segment`
231
+ Extract topics from transcript data.
232
+
233
+ **Request Body:**
234
+ ```json
235
+ {
236
+ "sentences": [
237
+ {
238
+ "text": "Your transcript text here",
239
+ "speaker": "Speaker name",
240
+ "start_time": 0.0,
241
+ "end_time": 5.0,
242
+ "sentence_index": 1
243
+ }
244
+ ],
245
+ "prompt_config": {
246
+ "template": "customer_call",
247
+ "language": "cs",
248
+ "business_domain": "E-commerce"
249
+ }
250
+ }
251
+ ```
252
+
253
+ #### `GET /docs`
254
+ Interactive API documentation (Swagger UI)
255
+
256
+ #### `GET /redoc`
257
+ Alternative API documentation (ReDoc)
258
+
259
+ ## 🔧 Integration Examples
260
+
261
+ ### cURL
262
+ ```bash
263
+ curl -X POST https://yeetek-anthropic-topic-segmentation.hf.space/segment \\
264
+ -H "Content-Type: application/json" \\
265
+ -d @your-request.json
266
+ ```
267
+
268
+ ### Python
269
+ ```python
270
+ import requests
271
+
272
+ response = requests.post(
273
+ "https://yeetek-anthropic-topic-segmentation.hf.space/segment",
274
+ json=your_request_data
275
+ )
276
+ result = response.json()
277
+ ```
278
+
279
+ ### n8n Workflow
280
+ Use the HTTP Request node with:
281
+ - **Method**: POST
282
+ - **URL**: https://yeetek-anthropic-topic-segmentation.hf.space/segment
283
+ - **Body**: JSON with your transcript data
284
+ """)
285
+
286
+ # Tab 4: Examples
287
+ with gr.Tab("💡 Examples"):
288
+ gr.Markdown("""
289
+ ## 🇨🇿 Czech E-commerce Example
290
+
291
+ Perfect for analyzing Shoptet integration discussions:
292
+
293
+ ```json
294
+ {
295
+ "sentences": [
296
+ {
297
+ "text": "Z��kazníci požadují nestandardní úpravy košíku v Shoptetu.",
298
+ "speaker": "Client",
299
+ "start_time": 2.01,
300
+ "end_time": 8.45,
301
+ "sentence_index": 1
302
+ },
303
+ {
304
+ "text": "Potřebujeme implementovat speciální platební metody.",
305
+ "speaker": "Client",
306
+ "start_time": 8.45,
307
+ "end_time": 15.2,
308
+ "sentence_index": 2
309
+ }
310
+ ],
311
+ "prompt_config": {
312
+ "template": "customer_call",
313
+ "language": "cs",
314
+ "business_domain": "E-commerce"
315
+ }
316
+ }
317
+ ```
318
+
319
+ ## 🇬🇧 English Business Interview
320
+
321
+ ```json
322
+ {
323
+ "sentences": [
324
+ {
325
+ "text": "Our main challenge is customer retention in the B2B segment.",
326
+ "speaker": "Manager",
327
+ "start_time": 0.0,
328
+ "end_time": 4.5,
329
+ "sentence_index": 1
330
+ },
331
+ {
332
+ "text": "We need better integration with existing CRM systems.",
333
+ "speaker": "Manager",
334
+ "start_time": 4.5,
335
+ "end_time": 8.2,
336
+ "sentence_index": 2
337
+ }
338
+ ],
339
+ "prompt_config": {
340
+ "template": "interview",
341
+ "language": "en",
342
+ "business_domain": "SaaS"
343
+ }
344
+ }
345
+ ```
346
+
347
+ ## 📊 Expected Output
348
+
349
+ The API returns structured business insights:
350
+
351
+ ```json
352
+ {
353
+ "status": "success",
354
+ "topics": [
355
+ {
356
+ "topic_name": "Nestandardní požadavky na košík",
357
+ "topic_type": "client_needs_b2b",
358
+ "topic_detail": "Zákazníci požadují nestandardní úpravy košíku...",
359
+ "confidence_score": 0.9,
360
+ "actionable_insights": [
361
+ "Vytvořit standardizovaný proces pro handling nestandardních požadavků"
362
+ ]
363
+ }
364
+ ],
365
+ "metadata": {
366
+ "processing_time": 10.5,
367
+ "topics_extracted": 3,
368
+ "average_confidence": 0.85
369
+ }
370
+ }
371
+ ```
372
+ """)
373
+
374
+ # Launch the app
375
+ if __name__ == "__main__":
376
+ app.launch(
377
+ server_name="0.0.0.0",
378
+ server_port=7860,
379
+ share=False,
380
+ show_error=True
381
+ )
models/input.py CHANGED
@@ -252,7 +252,8 @@ class TranscriptRequest(BaseModel):
252
  model_config = ConfigDict(
253
  str_strip_whitespace=True,
254
  validate_assignment=True,
255
- extra="forbid"
 
256
  )
257
 
258
  # Core transcript data
@@ -284,7 +285,8 @@ class TranscriptRequest(BaseModel):
284
 
285
  model_config_override: Optional[ModelConfiguration] = Field(
286
  default=None,
287
- description="Model configuration overrides"
 
288
  )
289
 
290
  # Processing options
 
252
  model_config = ConfigDict(
253
  str_strip_whitespace=True,
254
  validate_assignment=True,
255
+ extra="forbid",
256
+ protected_namespaces=()
257
  )
258
 
259
  # Core transcript data
 
285
 
286
  model_config_override: Optional[ModelConfiguration] = Field(
287
  default=None,
288
+ description="Model configuration overrides",
289
+ alias="model_config_override"
290
  )
291
 
292
  # Processing options
models/output.py CHANGED
@@ -236,7 +236,8 @@ class ProcessingMetadata(BaseModel):
236
  """
237
  model_config = ConfigDict(
238
  validate_assignment=True,
239
- extra="forbid"
 
240
  )
241
 
242
  # Request information
@@ -449,7 +450,8 @@ class HealthCheckResponse(BaseModel):
449
  """
450
  model_config = ConfigDict(
451
  validate_assignment=True,
452
- extra="forbid"
 
453
  )
454
 
455
  status: str = Field(
@@ -489,7 +491,8 @@ class ModelStatusResponse(BaseModel):
489
  """
490
  model_config = ConfigDict(
491
  validate_assignment=True,
492
- extra="forbid"
 
493
  )
494
 
495
  current_model: str = Field(
 
236
  """
237
  model_config = ConfigDict(
238
  validate_assignment=True,
239
+ extra="forbid",
240
+ protected_namespaces=()
241
  )
242
 
243
  # Request information
 
450
  """
451
  model_config = ConfigDict(
452
  validate_assignment=True,
453
+ extra="forbid",
454
+ protected_namespaces=()
455
  )
456
 
457
  status: str = Field(
 
491
  """
492
  model_config = ConfigDict(
493
  validate_assignment=True,
494
+ extra="forbid",
495
+ protected_namespaces=()
496
  )
497
 
498
  current_model: str = Field(
requirements.txt CHANGED
@@ -35,4 +35,8 @@ mypy==1.7.1
35
  psutil==5.9.6
36
 
37
  # SSL certificates fix
38
- certifi>=2023.0.0
 
 
 
 
 
35
  psutil==5.9.6
36
 
37
  # SSL certificates fix
38
+ certifi>=2023.0.0
39
+
40
+ # Gradio web interface for HuggingFace Spaces
41
+ gradio>=4.0.0
42
+ markdown>=3.5.0
start.sh ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Startup script for HuggingFace Spaces
4
+ # Runs both FastAPI backend and Gradio frontend
5
+
6
+ echo "🚀 Starting Anthropic Topic Segmentation Microservice..."
7
+
8
+ # Start FastAPI backend in the background
9
+ echo "📡 Starting FastAPI backend on port 8000..."
10
+ uvicorn app:app --host 0.0.0.0 --port 8000 --workers 1 &
11
+ FASTAPI_PID=$!
12
+
13
+ # Wait a moment for FastAPI to start
14
+ sleep 5
15
+
16
+ # Check if FastAPI is running
17
+ if curl -f http://localhost:8000/health > /dev/null 2>&1; then
18
+ echo "✅ FastAPI backend is healthy"
19
+ else
20
+ echo "❌ FastAPI backend failed to start"
21
+ exit 1
22
+ fi
23
+
24
+ # Start Gradio frontend on port 7860 (HuggingFace Spaces standard)
25
+ echo "🎨 Starting Gradio frontend on port 7860..."
26
+ python gradio_app.py
27
+
28
+ # If Gradio exits, also stop FastAPI
29
+ echo "🛑 Stopping services..."
30
+ kill $FASTAPI_PID 2>/dev/null || true
31
+ wait