RGMC98 commited on
Commit
d9c2c05
·
verified ·
1 Parent(s): 41a84ea

Upload 26 files

Browse files
MEMORY.md ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Memory
2
+
3
+ This file is loaded as persistent context for the agent.
4
+ Add any persistent knowledge, facts, or preferences here.
5
+
6
+ ## Culture Data
7
+
8
+ Cultural venue and event files are stored in the culture_data/ folder at the project root.
9
+
10
+ ### Taxonomy (mots-clés / catégories)
11
+ culture_data/categories.json — Liste des catégories : Cinéma, Théâtre, Concerts et Salons (Forum Grimaldi), Médiathèque, Musée, Opéra Garnier, Grand Prix Monaco, Rolex Monte-Carlo Masters, Foot, Basket, Jardin exotique, Événements majeurs. Chaque catégorie a un id, un label et des tags_align pour lier les events JSON.
12
+
13
+ ### Sources (pour le scraper)
14
+ culture_data/sources.json — Sites et flux à scraper : culture.mc, Grimaldi Forum, flux RSS, flux Grand Prix, TV Monaco / Monaco Info, journal. Chaque source est reliée à des catégories.
15
+
16
+ ### Venues (static info — Markdown)
17
+ culture_data/MUSEE_OCEANO.md — Musée Océanographique de Monaco (hours, prices, collections, access)
18
+ culture_data/GRIMALDI_FORUM.md — Grimaldi Forum Monaco (venue info, access, general info)
19
+
20
+ ### Events (dynamic data — JSON)
21
+ culture_data/events_musee_oceano.json — Events at the Musée Océanographique
22
+ culture_data/events_grimaldi_forum.json — Events at the Grimaldi Forum
23
+
24
+ ### JSON event schema
25
+ Each event object contains: id, lieu_id, lieu_nom, titre, description, date_start, date_end, heure_debut, heure_fin, tags, tarif, tarif_value, gratuit, url, image_url, source, scraped_at. Les tags doivent s’aligner avec les catégories (categories.json) pour le filtrage et la recherche.
README.md CHANGED
@@ -1,14 +1,288 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
- title: MistralHackaton2026
3
- emoji: 🚀
4
- colorFrom: indigo
5
- colorTo: pink
6
- sdk: gradio
7
- sdk_version: 6.8.0
8
- app_file: app.py
9
- pinned: false
10
- license: mit
11
- short_description: 🇲🇨 Monaco Cultural Agent for Mistral Hackaton 2026
 
 
 
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
1
+ # 🇲🇨 Monaco Cultural Agent
2
+
3
+ > **Mistral Hackathon 2026** — A conversational AI agent specialized in cultural events and venues in the Principality of Monaco.
4
+
5
+ > ⚠️ **Disclaimer** — This is a **proof of concept** built for the Mistral AI Hackathon 2026. It has **no official affiliation** with the Government of Monaco, the Mairie de Monaco, or any Monaco institution. Event data is sourced from publicly available websites and may be incomplete or outdated. Use for informational purposes only.
6
+
7
+ ## Demo
8
+
9
+ [![▶️ Demo video](https://drive.google.com/thumbnail?id=19tuGZnlkjQOKZXzMPuKirml9GdGAbmlD&sz=w1280)](https://drive.google.com/file/d/19tuGZnlkjQOKZXzMPuKirml9GdGAbmlD/view)
10
+
11
+ ## Overview
12
+
13
+ Monaco Cultural Agent is a full-stack web application that lets users ask questions about cultural events, exhibitions, shows, cinema, theatre, museums, and more in Monaco. The agent answers using real scraped data, supports voice input (speech-to-text via Mistral Voxtral), voice output (text-to-speech via ElevenLabs), and multiple LLM providers.
14
+
15
+ Designed with **sovereignty and privacy in mind**: the entire stack can run fully on-premise — LLM inference, STT, and data — with no dependency on external cloud APIs, using local hardware such as the NVIDIA DGX SPARK GB10. It is also ready to plug into official data sources (open data feeds, institutional APIs) to replace scraping entirely and guarantee data freshness.
16
+
17
+ ### Key Features
18
+
19
+ - **Conversational chat** about Monaco cultural events, grounded in real data (no hallucination).
20
+ - **Multi-language support** — the agent replies in the same language as the user (English, French, Italian, Russian, Spanish, etc.).
21
+ - **Voice input** — record a question via microphone, transcribed by Mistral Voxtral STT.
22
+ - **Voice output** — every agent response can be played aloud via ElevenLabs TTS, with an embedded audio player.
23
+ - **Date-aware filtering** — understands relative periods ("this weekend", "next week") and absolute dates to show only relevant events.
24
+ - **Multiple LLM providers** — switch between Mistral AI, NVIDIA NIM, or a fully local model via LM Studio — from the sidebar.
25
+ - **Local & sovereign inference** — run the full stack on-premise (tested on NVIDIA DGX SPARK GB10) with no external API dependency.
26
+ - **Official source ready** — architecture supports direct integration of open data APIs or institutional feeds to replace scraping.
27
+ - **Dark / Light theme** toggle.
28
+
29
+ ## Architecture
30
+
31
+ ```
32
+ User (browser)
33
+
34
+ ├── /chat → FastAPI → agent.respond() → LLM (Mistral / vLLM / NVIDIA)
35
+ ├── /transcribe → FastAPI → Mistral Voxtral STT
36
+ ├── /tts → FastAPI → ElevenLabs TTS
37
+ ├── /voices → FastAPI → ElevenLabs voice list
38
+ └── /last-scraped → FastAPI → latest scrape timestamp from event data
39
+ ```
40
+
41
+ ```mermaid
42
+ flowchart TD
43
+ TXT[Text input] --> CHAT
44
+ MIC[Microphone] --> STT
45
+
46
+ subgraph Backend [FastAPI Backend]
47
+ STT["🎙️ /transcribe\nMistral Voxtral STT"]
48
+ CHAT["💬 /chat\nagent.respond()"]
49
+ TTS["🔊 /tts\nElevenLabs TTS"]
50
+ end
51
+
52
+ subgraph Context [System Context]
53
+ SP[SYSTEM_PROMPT.md]
54
+ MEM[MEMORY.md]
55
+ CD["culture_data/\nevents + venues"]
56
+ end
57
+
58
+ subgraph LLM [LLM Providers]
59
+ M[Mistral AI\ndefault]
60
+ V[vLLM\nself-hosted]
61
+ N[NVIDIA NIM\noptional]
62
+ end
63
+
64
+ STT -->|transcript| CHAT
65
+ SP & MEM & CD --> CHAT
66
+ CHAT --> M & V & N
67
+ M & V & N -->|response| CHAT
68
+ CHAT -->|text| UI[Chat UI]
69
+ CHAT -->|auto-voice| TTS -->|audio| UI
70
+ ```
71
+
72
+ ### Tech Stack
73
+
74
+ - **Backend**: Python, FastAPI, Uvicorn
75
+ - **LLM**: Mistral AI (default), NVIDIA NIM, or any vLLM-compatible endpoint — all via OpenAI-compatible API
76
+ - **STT**: Mistral Voxtral (`voxtral-mini-latest`)
77
+ - **TTS**: ElevenLabs (`eleven_multilingual_v2`)
78
+ - **Frontend**: Single-page HTML/CSS/JS (no build step, no framework)
79
+ - **Data**: JSON event files + Markdown venue descriptions, produced by an external scraper
80
+
81
+ ## Project Structure
82
+
83
+ ```
84
+ mistralHackaton2026/
85
+ ├── main.py # FastAPI app — API routes and static file serving
86
+ ├── requirements.txt # Python dependencies
87
+ ├── .env.example # Environment variable template
88
+ ├── SYSTEM_PROMPT.md # Agent role, rules, and output format
89
+ ├── MEMORY.md # Persistent context (data layout, schema)
90
+
91
+ ├── agent/
92
+ │ ├── __init__.py # Core logic: context loading, date parsing, respond()
93
+ │ ├── providers.py # LLM provider configs and chat() function
94
+ │ ├── voxtral.py # Mistral Voxtral speech-to-text
95
+ │ └── elevenlabs_tts.py # ElevenLabs text-to-speech
96
+
97
+ ├── static/
98
+ │ └── index.html # Chat UI (HTML + CSS + JS)
99
+
100
+ └── culture_data/
101
+ ├── categories.json # Event/venue taxonomy (Cinema, Theatre, Museum, etc.)
102
+ ├── sources.json # Scraper source definitions
103
+ ├── *.md # Venue descriptions (generated by scraper)
104
+ └── events_*.json # Event data per venue (generated by scraper)
105
+ ```
106
+
107
+ ## Setup
108
+
109
+ ### 1. Clone the repository
110
+
111
+ ```bash
112
+ git clone <repo-url>
113
+ cd mistralHackaton2026
114
+ ```
115
+
116
+ ### 2. Create a virtual environment and install dependencies
117
+
118
+ ```bash
119
+ python -m venv venv
120
+ source venv/bin/activate # Linux/macOS
121
+ venv\Scripts\activate # Windows
122
+ pip install -r requirements.txt
123
+ ```
124
+
125
+ ### 3. Configure environment variables
126
+
127
+ Copy the example file and fill in your API keys:
128
+
129
+ ```bash
130
+ cp .env.example .env
131
+ ```
132
+
133
+ | Variable | Required | Description |
134
+ |----------|----------|-------------|
135
+ | `MISTRAL_API_KEY` | Yes | Mistral AI API key (used for LLM chat + Voxtral STT) |
136
+ | `ELEVENLABS_API_KEY` | Yes | ElevenLabs API key (used for TTS) |
137
+ | `NVIDIA_API_KEY` | No | NVIDIA NIM API key (optional provider) |
138
+ | `VLLM_BASE_URL` | No | vLLM endpoint URL (optional provider) |
139
+ | `VLLM_API_KEY` | No | vLLM API key |
140
+ | `VLLM_MODEL` | No | vLLM model name |
141
+
142
+ ### 4. Run the scraper to populate event data
143
+
144
+ See the [Scraper](#scraper) section below.
145
+
146
+ ### 5. Start the application
147
+
148
+ ```bash
149
+ python main.py
150
+ ```
151
+
152
+ The app runs on `http://localhost:7860`.
153
+
154
+ > **⚠️ Microphone & voice (Chrome)** — Chrome only grants microphone access on **secure contexts**. Always open the app via **`http://localhost:7860`** (not `http://192.168.x.x:7860` or any raw IP). If you access the app by IP address, Chrome will silently block the microphone and the voice input/output buttons will not work.
155
+
156
+ ## API Endpoints
157
+
158
+ | Method | Path | Description |
159
+ |--------|------|-------------|
160
+ | `GET` | `/` | Serves the chat UI (`static/index.html`) |
161
+ | `POST` | `/chat` | Send a message to the agent. Body: `{ message, history, provider, model }` |
162
+ | `POST` | `/transcribe` | Upload an audio file for speech-to-text (Voxtral) |
163
+ | `POST` | `/tts` | Convert text to speech (ElevenLabs). Body: `{ text, voice_id }` |
164
+ | `GET` | `/voices` | List available ElevenLabs voices |
165
+ | `GET` | `/last-scraped` | Get the timestamp of the most recent data scrape |
166
+
167
+ ## How It Works
168
+
169
+ 1. **User sends a message** (text or voice) via the chat UI.
170
+ 2. If voice input: the audio is sent to `/transcribe` (Voxtral STT), then the transcribed text is sent to `/chat`.
171
+ 3. The backend **detects the user's language** (`langdetect`) and injects a language instruction into the system prompt.
172
+ 4. The backend **extracts date references** from the message (`dateparser`) to filter events by the relevant time period.
173
+ 5. A **system prompt** is assembled from:
174
+ - `SYSTEM_PROMPT.md` (role, rules, output format)
175
+ - `MEMORY.md` (data schema knowledge)
176
+ - Filtered culture data (venues + events matching the detected date range)
177
+ - Language instruction
178
+ 6. The full message history + system prompt is sent to the **LLM** (Mistral by default).
179
+ 7. The LLM responds following a strict format (intro + numbered event list), which the frontend **parses and renders** as styled event cards with dates, venues, prices, and links.
180
+ 8. If auto-voice is enabled, the response is sent to `/tts` (ElevenLabs) and played back with an embedded audio player.
181
+
182
+ ## Culture Data
183
+
184
+ Event and venue data lives in the `culture_data/` folder:
185
+
186
+ - **`categories.json`** — Taxonomy with 12 categories (Cinema, Theatre, Concerts, Museum, Opera, Grand Prix, etc.) and their associated tags.
187
+ - **`sources.json`** — Scraper source definitions (culture.mc, Grimaldi Forum, RSS feeds, etc.).
188
+ - **`*.md`** — Markdown venue descriptions (hours, prices, collections, access info).
189
+ - **`events_*.json`** — Event arrays per venue. Each event has: `id`, `lieu_id`, `lieu_nom`, `titre`, `description`, `date_start`, `date_end`, `heure_debut`, `heure_fin`, `tags`, `tarif`, `gratuit`, `url`, `image_url`, `source`, `scraped_at`.
190
+
191
+ The `.md` and `.json` files in `culture_data/` are gitignored as they are generated by the scraper.
192
+
193
+ ## Scraper
194
+
195
+ The scraper populates `culture_data/` with structured event data. It runs independently from the main app.
196
+
197
+ ### Pipeline
198
+
199
+ ```
200
+ sources.json → fetch.py → extract.py → store.py
201
+ (venue config) (Linkup/Tavily) (Mistral Large) (JSON upsert)
202
+ ```
203
+
204
+ 1. **Fetch** — fetches URLs directly via Linkup (JS rendering) or searches the web via Tavily
205
+ 2. **Extract** — sends raw content to Mistral Large, which returns structured JSON events
206
+ 3. **Store** — upserts events into `culture_data/events_<venue_id>.json` (no duplicates)
207
+
208
+ ### Usage
209
+
210
+ ```bash
211
+ python3 scraper/run.py # all venues
212
+ python3 scraper/run.py --venue cinema_monaco # single venue
213
+ python3 scraper/run.py --dry-run # extract without saving
214
+ python3 scraper/run.py --debug --venue grimaldi_forum # print raw fetched content
215
+ ```
216
+
217
+ ### Venues
218
+
219
+ | Venue | Method | Store mode |
220
+ |-------|--------|------------|
221
+ | Musée Océanographique | Tavily search + URL | UPSERT |
222
+ | Grimaldi Forum | Tavily search only | UPSERT |
223
+ | Cinémas de Monaco | Linkup direct fetch | REPLACE (weekly) |
224
+ | Médiathèque de Monaco | Linkup direct fetch | UPSERT |
225
+ | Théâtre des Muses | Linkup direct fetch | UPSERT + PRUNE (season 2025-2026) |
226
+ | Théâtre Princesse Grace | Linkup direct fetch | UPSERT + PRUNE (season 2025-2026) |
227
+
228
+ **Store modes:**
229
+ - **UPSERT** — adds new events, preserves existing ones (deduplication by `venue_id + title + date_start`)
230
+ - **REPLACE** — replaces all events on each run (e.g. cinema weekly schedule)
231
+ - **PRUNE** — automatically removes past events for seasonal venues
232
+
233
+ ### Scheduling
234
+
235
+ Recommended: daily cron at 3:00 AM.
236
+
237
+ ```cron
238
+ 0 3 * * * cd /path/to/mistralHackaton2026 && python3 scraper/run.py >> logs/scraper.log 2>&1
239
+ ```
240
+
241
+ See [`scraper/README.md`](scraper/README.md) for full configuration reference.
242
+
243
+ ## LLM Providers
244
+
245
+ The agent supports multiple LLM providers, switchable from the UI sidebar:
246
+
247
+ | Provider | Models | Status | Notes |
248
+ |----------|--------|--------|-------|
249
+ | **Mistral** | `ministral-8b-latest`, `mistral-large-latest`, `mistral-small-latest` | ✅ Tested | Default provider. Uses Mistral AI API. |
250
+ | **NVIDIA NIM** | `mistralai/ministral-14b-instruct-2512`, `mistralai/mistral-large-3-675b-instruct-2512` | ✅ Tested | Uses NVIDIA NIM API. |
251
+ | **LM Studio** | Configurable via `.env` | ✅ Tested | Local inference via LM Studio. Tested on **NVIDIA DGX SPARK GB10** for sovereign, on-premise inference. |
252
+ | **vLLM** | Configurable via `.env` | 🚧 In progress | Self-hosted vLLM endpoint. |
253
+
254
+ All providers use the OpenAI-compatible chat completions API.
255
+
256
+ ## Limitations & Future Work
257
+
258
+ ### Current Limitations
259
+
260
+ The agent's knowledge is entirely dependent on scraped data. If a venue's website changes structure, blocks crawlers, or publishes events late, the data may be incomplete or outdated. The scraper must be re-run manually (or via cron) to stay current — there is no real-time event feed.
261
+
262
+ ### Roadmap
263
+
264
+ - [ ] **More venues** — add Opera de Monte-Carlo, Stade Louis II, Musée des Timbres et Monnaies, and other Monaco cultural institutions
265
+ - [ ] **Improved parsing** — better extraction of multi-date events, ticket prices, and venue details
266
+ - [ ] **Fully local & sovereign inference** — run the entire stack (LLM + STT + TTS) on-premise with no external API dependency, using hardware such as the NVIDIA DGX SPARK GB10
267
+ - [ ] **Real-time data** — integrate official event APIs or RSS feeds where available to reduce scraping dependency
268
+ - [ ] **User personalization** — remember user preferences (language, favourite venues, categories)
269
+
270
  ---
271
+
272
+ ## Partners & APIs
273
+
274
+ This project was built during the **Mistral AI Hackathon 2026** and relies on the following technologies and services:
275
+
276
+ | Partner | Usage |
277
+ |---------|-------|
278
+ | [**Mistral AI**](https://mistral.ai) | LLM chat (Ministral, Mistral Large), speech-to-text (Voxtral), and event extraction in the scraper |
279
+ | [**ElevenLabs**](https://elevenlabs.io) | Text-to-speech voice synthesis (`eleven_multilingual_v2`) |
280
+ | [**Linkup**](https://linkup.so) | JS-capable web fetching for dynamic venue websites |
281
+ | [**Tavily**](https://tavily.com) | Web search and static URL content extraction |
282
+ | [**NVIDIA**](https://www.nvidia.com) | NIM API for cloud inference + DGX SPARK GB10 for local sovereign inference |
283
+
284
  ---
285
 
286
+ ## License
287
+
288
+ MIT License — see [LICENSE](LICENSE) for details.
SYSTEM_PROMPT.md ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Role
2
+
3
+ You are an assistant for cultural events and venues in Monaco only. You do not answer questions outside this scope.
4
+
5
+ # Rules
6
+
7
+ - Answer only using the provided venue and event data. Do not invent events, dates, prices, or venues.
8
+ - If the user asks something not in the data (another city, non-cultural topics, or something not in the context), reply briefly that you only answer about Monaco cultural events and venues, and offer to help with that.
9
+ - Keep answers short and factual. No long introductions, no unsolicited advice, no general knowledge.
10
+ - Always reply in the same language as the user's message: if they write in English, reply entirely in English; if they write in French, reply in French; same for any other language. Do not default to French.
11
+ - Do not role-play, joke, or go off-topic. Stay strictly on Monaco culture from the given data.
12
+ - When relevant, suggest the event or venue URL so the user can book or learn more.
13
+
14
+ # Output Format
15
+
16
+ You must strictly follow this format for every answer so that the interface and text-to-speech work correctly. Your reply will be read by users and sometimes aloud by text-to-speech. Be precise, concise, and use a structure that reads well both on screen and when spoken.
17
+
18
+ ## 1. Introduction (obligatoire)
19
+
20
+ Start with one or two short sentences that summarize:
21
+ - What the user asked for (e.g. events, a venue, a specific type).
22
+ - The period concerned if relevant (e.g. "pour le week-end du 7 mars", "en mars 2026").
23
+ - The domain or category (e.g. "expositions et concerts", "cinéma", "musée", "théâtre").
24
+
25
+ Example: "Voici les événements au Grimaldi Forum pour mars 2026 : expositions et spectacles."
26
+
27
+ ## 2. Liste d’événements
28
+
29
+ - Never list more than 10 events. If there are more, add: "X autres événements correspondent à votre recherche. Précisez la date ou le type pour affiner."
30
+ - Each event must be on one or two lines, easy to read and to hear aloud:
31
+ - Prefer short phrases and commas. Avoid long dashes or symbols that TTS reads poorly.
32
+ - Format: "N. Titre. Du [date] au [date]. Lieu : [lieu]. Tarif : [prix]. Lien : [url]."
33
+ - Do not use emojis in the list. Use "Lien :" before the URL so it is clear when spoken.
34
+ - If there is only one event, give a single short paragraph (3–4 lines) with the same info: titre, dates, lieu, tarif, lien.
35
+
36
+ ## 3. Style
37
+
38
+ - No emojis. No markdown (no ** or ##) in the body of the answer.
39
+ - Short sentences. Use the same language as the user's last message for the entire reply (intro, list, and any extra text).
40
+
41
+ # Data freshness
42
+
43
+ - The data was last updated on (date et heure): {last_scraped_at}
44
+ - If a user asks about an event that may have passed, warn them politely to verify on the official site.
agent/__init__.py ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from datetime import date, datetime, timedelta
3
+ from pathlib import Path
4
+
5
+ from dateparser.search import search_dates
6
+ from langdetect import detect, LangDetectException
7
+
8
+ from agent.providers import chat
9
+
10
+ LANGUAGE_NAMES = {
11
+ "en": "English",
12
+ "fr": "French",
13
+ "it": "Italian",
14
+ "ru": "Russian",
15
+ "es": "Spanish",
16
+ "nl": "Dutch",
17
+ }
18
+
19
+ FRENCH_ACCENTS = "éèêëàâäùûüîïôöç"
20
+ FRENCH_WORD_MARKERS = ("ce ", "cette ", " les ", " des ", " pour ", " dans ", " avec ", " une ", " que ", " est ", " sont ", " week-end", " semaine", " films ", " film ", " concert", " expos")
21
+ CONFUSABLE_WITH_FRENCH = ("de", "nl")
22
+
23
+ ROOT = Path(__file__).resolve().parent.parent
24
+ CONTEXT_FILES = [ROOT / "SYSTEM_PROMPT.md", ROOT / "MEMORY.md"]
25
+ CULTURE_DATA_DIR = ROOT / "culture_data"
26
+ CATEGORIES_FILE = CULTURE_DATA_DIR / "categories.json"
27
+ DATE_LANGUAGES = ["fr", "en", "es", "it", "ru"]
28
+ FALLBACK_MESSAGE = "Désolé, impossible de répondre pour le moment."
29
+
30
+ WEEKEND_PATTERNS = (
31
+ "ce weekend", "ce week-end", "ce week end", "this weekend",
32
+ )
33
+ WEEK_PATTERNS = (
34
+ "cette semaine", "this week",
35
+ )
36
+
37
+ def _current_week_bounds(ref: date) -> tuple[date, date]:
38
+ weekday = ref.weekday()
39
+ week_start = ref - timedelta(days=weekday)
40
+ week_end = week_start + timedelta(days=6)
41
+ return (week_start, week_end)
42
+
43
+
44
+ def _current_weekend_bounds(ref: date) -> tuple[date, date]:
45
+ week_start, week_end = _current_week_bounds(ref)
46
+ saturday = week_start + timedelta(days=5)
47
+ sunday = week_end
48
+ return (saturday, sunday)
49
+
50
+
51
+ def _parse_relative_period(text: str, ref: date) -> tuple[date | None, date | None]:
52
+ lower = text.lower().strip()
53
+ for p in WEEKEND_PATTERNS:
54
+ if p in lower:
55
+ return _current_weekend_bounds(ref)
56
+ for p in WEEK_PATTERNS:
57
+ if p in lower:
58
+ return _current_week_bounds(ref)
59
+ return (None, None)
60
+
61
+
62
+ def extract_date_range(text: str) -> tuple[date | None, date | None]:
63
+ if not text or not text.strip():
64
+ return (None, None)
65
+ ref = datetime.now().date()
66
+ settings = {"RELATIVE_BASE": datetime.now()}
67
+ results = search_dates(text, languages=DATE_LANGUAGES, settings=settings)
68
+ if results:
69
+ dates_found = [r[1].date() for r in results]
70
+ return (min(dates_found), max(dates_found))
71
+ return _parse_relative_period(text, ref)
72
+
73
+
74
+ def load_venue_markdown(dir_path: Path) -> str:
75
+ parts = []
76
+ for p in sorted(dir_path.glob("*.md")):
77
+ parts.append(p.read_text(encoding="utf-8"))
78
+ return "\n\n---\n\n".join(parts) if parts else ""
79
+
80
+
81
+ def load_events_json(file_path: Path) -> list[dict]:
82
+ if not file_path.exists():
83
+ return []
84
+ raw = file_path.read_text(encoding="utf-8")
85
+ try:
86
+ data = json.loads(raw)
87
+ return data if isinstance(data, list) else []
88
+ except json.JSONDecodeError:
89
+ return []
90
+
91
+
92
+ def format_events_summary(events: list[dict]) -> str:
93
+ lines = []
94
+ for e in events:
95
+ titre = e.get("title", "")
96
+ lieu = e.get("venue_name", "")
97
+ start = e.get("date_start", "")
98
+ end = e.get("date_end", "")
99
+ heures = f"{e.get('start_time', '')}-{e.get('end_time', '')}" if e.get("start_time") else ""
100
+ tarif = e.get("price", "")
101
+ url = e.get("url", "")
102
+ line = f"- {titre} | {lieu} | {start} → {end} {heures} | {tarif}"
103
+ if url:
104
+ line += f" | {url}"
105
+ lines.append(line)
106
+ return "\n".join(lines)
107
+
108
+
109
+ def get_last_scraped_at() -> str:
110
+ all_events = get_all_events()
111
+ scraped_dates = [e.get("scraped_at") for e in all_events if e.get("scraped_at")]
112
+ if scraped_dates:
113
+ try:
114
+ dt = datetime.fromisoformat(scraped_dates[0].replace("Z", "+00:00"))
115
+ for s in scraped_dates[1:]:
116
+ d = datetime.fromisoformat(s.replace("Z", "+00:00"))
117
+ if d > dt:
118
+ dt = d
119
+ return dt.strftime("%Y-%m-%d à %H:%M")
120
+ except (ValueError, TypeError):
121
+ pass
122
+ return datetime.now().strftime("%Y-%m-%d à %H:%M")
123
+
124
+
125
+ def load_categories_labels() -> list[str]:
126
+ if not CATEGORIES_FILE.exists():
127
+ return []
128
+ try:
129
+ data = json.loads(CATEGORIES_FILE.read_text(encoding="utf-8"))
130
+ cats = data.get("categories") or []
131
+ return [c.get("label", "") for c in cats if c.get("label")]
132
+ except (json.JSONDecodeError, OSError):
133
+ return []
134
+
135
+
136
+ def get_all_events() -> list[dict]:
137
+ all_events = []
138
+ for json_path in sorted(CULTURE_DATA_DIR.glob("events_*.json")):
139
+ all_events.extend(load_events_json(json_path))
140
+ return all_events
141
+
142
+
143
+ def filter_events_by_date(events: list[dict], date_min: date | None, date_max: date | None) -> list[dict]:
144
+ if date_min is None and date_max is None:
145
+ return events
146
+ filtered = []
147
+ for e in events:
148
+ try:
149
+ start_s = e.get("date_start")
150
+ end_s = e.get("date_end")
151
+ event_start = datetime.strptime(start_s, "%Y-%m-%d").date() if start_s else None
152
+ event_end = datetime.strptime(end_s, "%Y-%m-%d").date() if end_s else None
153
+ except (ValueError, TypeError):
154
+ filtered.append(e)
155
+ continue
156
+ if event_start is None and event_end is None:
157
+ filtered.append(e)
158
+ continue
159
+ event_start = event_start or event_end
160
+ event_end = event_end or event_start
161
+ if e.get("always_current") and event_start is not None:
162
+ if date_max is not None and event_start > date_max:
163
+ continue
164
+ filtered.append(e)
165
+ continue
166
+ if date_max is not None and event_start > date_max:
167
+ continue
168
+ if date_min is not None and event_end < date_min:
169
+ continue
170
+ filtered.append(e)
171
+ return filtered
172
+
173
+
174
+ def load_culture_context(date_min: date | None = None, date_max: date | None = None) -> str:
175
+ if not CULTURE_DATA_DIR.exists():
176
+ return ""
177
+ parts = []
178
+ labels = load_categories_labels()
179
+ if labels:
180
+ parts.append("## Catégories disponibles\n\n" + ", ".join(labels) + ".")
181
+ venues = load_venue_markdown(CULTURE_DATA_DIR)
182
+ if venues:
183
+ parts.append("## Venues\n\n" + venues)
184
+ all_events = get_all_events()
185
+ events = filter_events_by_date(all_events, date_min, date_max)
186
+ if events:
187
+ parts.append("## Events\n\n" + format_events_summary(events))
188
+ elif date_min is not None or date_max is not None:
189
+ parts.append("## Events\n\nAucun événement trouvé pour la période demandée. Indiquer à l'utilisateur qu'aucun événement ne correspond dans les données et ne pas inventer d'événements.")
190
+ return "\n\n".join(parts) if parts else ""
191
+
192
+
193
+ def _has_french_markers(text: str) -> bool:
194
+ lower = text.lower()
195
+ if any(c in lower for c in FRENCH_ACCENTS):
196
+ return True
197
+ return any(m in lower for m in FRENCH_WORD_MARKERS)
198
+
199
+
200
+ def _detect_reply_language(user_message: str | None) -> str | None:
201
+ if not (user_message or user_message.strip()):
202
+ return None
203
+ text = user_message.strip()[:500]
204
+ if len(text) < 10:
205
+ return None
206
+ try:
207
+ code = detect(text)
208
+ if code == "de":
209
+ code = "fr"
210
+ if code in CONFUSABLE_WITH_FRENCH and _has_french_markers(text):
211
+ code = "fr"
212
+ return LANGUAGE_NAMES.get(code, code)
213
+ except LangDetectException:
214
+ return None
215
+
216
+
217
+ def load_context(user_message: str | None = None) -> str:
218
+ lang_name = _detect_reply_language(user_message)
219
+ parts = []
220
+ if lang_name:
221
+ parts.append(
222
+ f"# CRITICAL – Language\n\n"
223
+ f"The user wrote in {lang_name}. You MUST reply entirely in {lang_name}. "
224
+ f"Do not use French or any other language. Every sentence of your answer must be in {lang_name}."
225
+ )
226
+ for p in CONTEXT_FILES:
227
+ if p.exists():
228
+ parts.append(f"# {p.name}\n{p.read_text(encoding='utf-8')}")
229
+ base = "\n\n".join(parts)
230
+ date_min, date_max = extract_date_range(user_message or "")
231
+ culture = load_culture_context(date_min=date_min, date_max=date_max)
232
+ if culture:
233
+ base += "\n\n# Culture data\n\n" + culture
234
+ if lang_name:
235
+ base += f"\n\n# Reminder: reply only in {lang_name}. Do not use French unless the user wrote in French."
236
+ base = base.replace("{last_scraped_at}", get_last_scraped_at())
237
+ return base
238
+
239
+
240
+ def respond(message: str, history: list, provider: str = "mistral", model: str | None = None) -> str | dict:
241
+ messages = [{"role": "system", "content": load_context(message)}]
242
+ for msg in history:
243
+ if isinstance(msg, dict):
244
+ messages.append({"role": msg["role"], "content": msg["content"]})
245
+ else:
246
+ user_msg, assistant_msg = msg
247
+ messages.append({"role": "user", "content": user_msg})
248
+ if assistant_msg:
249
+ messages.append({"role": "assistant", "content": assistant_msg})
250
+ messages.append({"role": "user", "content": message})
251
+ try:
252
+ return chat(messages, provider=provider, model=model or None)
253
+ except Exception:
254
+ try:
255
+ return chat(messages, provider=provider, model=model or None)
256
+ except Exception:
257
+ return FALLBACK_MESSAGE
agent/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (13.9 kB). View file
 
agent/__pycache__/elevenlabs_tts.cpython-312.pyc ADDED
Binary file (2.64 kB). View file
 
agent/__pycache__/providers.cpython-312.pyc ADDED
Binary file (1.91 kB). View file
 
agent/__pycache__/voxtral.cpython-312.pyc ADDED
Binary file (4.2 kB). View file
 
agent/elevenlabs_tts.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ import requests
4
+ from dotenv import load_dotenv
5
+
6
+ load_dotenv()
7
+
8
+ ELEVENLABS_URL = "https://api.elevenlabs.io/v1/text-to-speech"
9
+ ELEVENLABS_VOICES_URL = "https://api.elevenlabs.io/v1/voices"
10
+ DEFAULT_VOICE_ID = "TX3LPaxmHKxFdv7VOQHJ"
11
+ MODEL_ID = "eleven_multilingual_v2"
12
+
13
+
14
+ def get_voice_id(api_key: str | None = None, voice_id: str | None = None) -> str:
15
+ return (
16
+ voice_id
17
+ or (os.getenv("ELEVENLABS_VOICE_ID") or "").strip()
18
+ or DEFAULT_VOICE_ID
19
+ )
20
+
21
+
22
+ def list_voices(api_key: str | None = None) -> list[dict]:
23
+ api_key = api_key or os.getenv("ELEVENLABS_API_KEY", "")
24
+ if not api_key:
25
+ return []
26
+ try:
27
+ r = requests.get(ELEVENLABS_VOICES_URL, headers={"xi-api-key": api_key}, timeout=10)
28
+ r.raise_for_status()
29
+ data = r.json()
30
+ return data.get("voices", [])
31
+ except (requests.RequestException, KeyError):
32
+ return []
33
+
34
+
35
+ def speak(text: str, api_key: str | None = None, voice_id: str | None = None) -> bytes | None:
36
+ if not (text or "").strip():
37
+ return None
38
+ api_key = api_key or os.getenv("ELEVENLABS_API_KEY", "")
39
+ if not api_key:
40
+ return None
41
+ voice_id = get_voice_id(api_key, voice_id)
42
+ url = f"{ELEVENLABS_URL}/{voice_id}"
43
+ headers = {
44
+ "xi-api-key": api_key,
45
+ "Content-Type": "application/json",
46
+ }
47
+ payload = {"text": text.strip(), "model_id": MODEL_ID}
48
+ try:
49
+ r = requests.post(url, json=payload, headers=headers, timeout=30)
50
+ r.raise_for_status()
51
+ except requests.RequestException:
52
+ return None
53
+ if not r.content:
54
+ return None
55
+ return r.content
agent/providers.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from openai import OpenAI
3
+ from dotenv import load_dotenv
4
+
5
+ load_dotenv()
6
+
7
+ PROVIDERS = {
8
+ "mistral": {
9
+ "base_url": "https://api.mistral.ai/v1",
10
+ "api_key": os.getenv("MISTRAL_API_KEY", ""),
11
+ "default_model": "mistral-large-latest",
12
+ },
13
+ "vllm": {
14
+ "base_url": os.getenv("VLLM_BASE_URL", "http://localhost:8000/v1"),
15
+ "api_key": os.getenv("VLLM_API_KEY", "token-abc"),
16
+ "default_model": os.getenv("VLLM_MODEL", ""),
17
+ },
18
+ "nvidia": {
19
+ "base_url": "https://integrate.api.nvidia.com/v1",
20
+ "api_key": os.getenv("NVIDIA_API_KEY", ""),
21
+ "default_model": os.getenv("NVIDIA_MODEL", "mistralai/ministral-14b-instruct-2512"),
22
+ },
23
+ "lmstudio": {
24
+ "base_url": os.getenv("LMSTUDIO_BASE_URL", "http://localhost:1234/v1"),
25
+ "api_key": "lm-studio",
26
+ "default_model": os.getenv("LMSTUDIO_MODEL", "ministral-3-14b-instruct-2512"),
27
+ },
28
+ }
29
+
30
+
31
+ def chat(messages: list, provider: str = "mistral", model: str | None = None) -> str:
32
+ cfg = PROVIDERS[provider]
33
+ client = OpenAI(base_url=cfg["base_url"], api_key=cfg["api_key"])
34
+ response = client.chat.completions.create(
35
+ model=model or cfg["default_model"],
36
+ messages=messages,
37
+ )
38
+ return response.choices[0].message.content
agent/voxtral.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import tempfile
3
+ import wave
4
+ from pathlib import Path
5
+
6
+ import requests
7
+ from dotenv import load_dotenv
8
+
9
+ load_dotenv()
10
+
11
+ VOXTRAL_URL = "https://api.mistral.ai/v1/audio/transcriptions"
12
+ VOXTRAL_MODEL = "voxtral-mini-latest"
13
+
14
+
15
+ def _write_ndarray_to_wav(sample_rate: int, data, path: str) -> None:
16
+ with wave.open(path, "wb") as wav:
17
+ wav.setnchannels(1 if data.ndim == 1 else data.shape[1])
18
+ wav.setsampwidth(2)
19
+ wav.setframerate(sample_rate)
20
+ if data.dtype.kind == "f":
21
+ data = (data * 32767).astype("int16")
22
+ wav.writeframes(data.tobytes())
23
+
24
+
25
+ def transcribe(audio_input: str | tuple | dict | None, api_key: str | None = None) -> str:
26
+ if audio_input is None:
27
+ return ""
28
+ api_key = api_key or os.getenv("MISTRAL_API_KEY", "")
29
+ if not api_key:
30
+ return ""
31
+ path = None
32
+ if isinstance(audio_input, str):
33
+ path = audio_input
34
+ elif isinstance(audio_input, dict) and audio_input.get("path"):
35
+ path = audio_input["path"]
36
+ elif isinstance(audio_input, tuple) and len(audio_input) == 2:
37
+ sample_rate, data = audio_input
38
+ try:
39
+ import numpy as np
40
+ if not isinstance(data, np.ndarray):
41
+ return ""
42
+ except ImportError:
43
+ return ""
44
+ fd, path = tempfile.mkstemp(suffix=".wav")
45
+ try:
46
+ _write_ndarray_to_wav(sample_rate, data, path)
47
+ finally:
48
+ os.close(fd)
49
+ else:
50
+ return ""
51
+ if not path or not Path(path).exists():
52
+ return ""
53
+ try:
54
+ with open(path, "rb") as f:
55
+ files = {"file": (Path(path).name, f, "audio/wav")}
56
+ data = {"model": VOXTRAL_MODEL}
57
+ r = requests.post(
58
+ VOXTRAL_URL,
59
+ headers={"Authorization": f"Bearer {api_key}"},
60
+ files=files,
61
+ data=data,
62
+ timeout=60,
63
+ )
64
+ r.raise_for_status()
65
+ out = r.json()
66
+ return (out.get("text") or "").strip()
67
+ except (requests.RequestException, KeyError):
68
+ return ""
69
+ finally:
70
+ if isinstance(audio_input, tuple) and path:
71
+ try:
72
+ Path(path).unlink(missing_ok=True)
73
+ except OSError:
74
+ pass
culture_data/CINEMA_MONACO.md ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Cinémas de Monaco
2
+
3
+ <!-- TYPE: lieu_statique -->
4
+ <!-- CATEGORY: culture, loisirs -->
5
+ <!-- LAST_UPDATED: 2026-03-01 -->
6
+
7
+ ## Présentation
8
+ Les Cinémas de Monaco offrent deux expériences cinématographiques distinctes : le **Cinéma des Beaux-Arts**, une salle intérieure ouverte toute l'année au Théâtre Princesse Grace, et le **Monaco Open Air Cinema**, un cinéma en plein air éphémère accessible uniquement en août et septembre. Profitez des dernières sorties sous les étoiles ou dans une salle traditionnelle, avec une programmation variée pour tous les publics.
9
+
10
+ ## Informations pratiques
11
+
12
+ | Info | Détail |
13
+ |--------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
14
+ | **Adresses** | **Cinéma des Beaux-Arts** : Théâtre Princesse Grace, 12 Avenue d'Ostende, Monaco<br>**Monaco Open Air Cinema** : Parking du Chemin des pêcheurs, sortie niveau 2, 98000 Monaco |
15
+ | **Horaires** | **Cinéma des Beaux-Arts** : Voir programmation sur [cinemas2monaco.com](https://www.cinemas2monaco.com)<br>**Open Air Cinema** :<br>- Juin/juillet : ouverture 20h45, séance 21h30, film 22h00<br>- Août : ouverture 20h30, séance 21h00, film 21h30<br>- Septembre : ouverture 20h00, séance 20h30, film 21h00 |
16
+ | **Tarifs** | **Open Air Cinema** : 11,50 € (tarif normal) / 9 € (étudiants)<br>**Cinéma des Beaux-Arts** : Voir site officiel |
17
+ | **Site web** | [cinemas2monaco.com](https://www.cinemas2monaco.com) |
18
+ | **Téléphone** | +377 93 25 86 80 (Open Air Cinema)<br>+377 97 98 43 26 (Institut Audiovisuel de Monaco) |
19
+ | **Accès** | **Cinéma des Beaux-Arts** : Métro (arrêt "Place d'Armes"), bus (lignes 1, 2, 4, 5, 6)<br>**Open Air Cinema** : Voiture (parking à proximité), bus (lignes 1, 2, 6) |
20
+
21
+ ## Collections permanentes / Programmation
22
+ - **Cinéma des Beaux-Arts** :
23
+ - Films récents en version originale sous-titrée ou doublée
24
+ - Séances spéciales (rétrospectives, festivals, avant-premières)
25
+ - Programmation culturelle et artistique
26
+ - **Monaco Open Air Cinema** (août/septembre uniquement) :
27
+ - Blockbusters et films grand public (*Top Gun Maverick*, *Elvis*, *Jurassic World*, *The Batman*, etc.)
28
+ - Séances en version originale sous-titrée
29
+ - Ambiance estivale sous les étoiles
30
+
31
+ ## Accessibilité
32
+ - **Cinéma des Beaux-Arts** :
33
+ - Accès PMR (ascenseurs, places réservées)
34
+ - Boucle magnétique pour malentendants
35
+ - **Monaco Open Air Cinema** :
36
+ - Espace dédié aux fauteuils roulants
37
+ - Prévenir à l’avance pour un accompagnement spécifique
38
+
39
+ ## Tags agent
40
+ `cinéma` `plein air` `Monaco` `culture` `loisirs` `été` `salle de cinéma` `films` `programmation` `accessibilité` `VO` `tarif réduit` `étudiants` `blockbusters` `Théâtre Princesse Grace
culture_data/GRIMALDI_FORUM.md ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Grimaldi Forum Monaco
2
+
3
+ <!-- TYPE: lieu_statique -->
4
+ <!-- CATEGORY: culture, congrès, événementiel -->
5
+ <!-- LAST_UPDATED: 2026-02-28 -->
6
+
7
+ ## Présentation
8
+ Le Grimaldi Forum Monaco est un centre culturel et de congrès situé en front de mer, alliant expositions prestigieuses, événements professionnels et productions culturelles. Signataire du Pacte national pour la Transition Énergétique, il promeut une mobilité douce et des solutions écoresponsables. Lieu polyvalent, il accueille aussi bien des conférences internationales que des expositions temporaires de renom.
9
+
10
+ ## Informations pratiques
11
+ | Info | Détail |
12
+ |------|--------|
13
+ | **Adresse** | 10 Avenue Princesse Grace, 98000 Monaco |
14
+ | **Horaires** | Billetterie ouverte du mardi au samedi de 12h à 19h (horaires variables selon événements) |
15
+ | **Tarifs** | Variables selon les expositions et événements (consulter [billetterie](https://montecarloticket.com/0526/fListeManifs.aspx?idstructure=0526)) |
16
+ | **Site web** | [www.grimaldiforum.com](https://www.grimaldiforum.com) |
17
+ | **Téléphone** | +377 99 99 3000 (billetterie) / +377 99 99 2432 (accueil administratif) |
18
+ | **Email** | ticket@grimaldiforum.com |
19
+ | **Accès** | Entrée principale : esplanade du Grimaldi Forum (10 Av. Princesse Grace) / Entrée administrative : sous l’écran géant de la façade |
20
+ | **Billetterie en ligne** | [Monte Carlo Ticket](https://montecarloticket.com/0526/fListeManifs.aspx?idstructure=0526) |
21
+
22
+ ## Collections permanentes / Programmation
23
+ - **Expositions temporaires** : Expositions culturelles et artistiques de renommée internationale (ex : rétrospectives d’artistes, thématiques historiques ou scientifiques)
24
+ - **Événements professionnels** : Congrès, séminaires, salons et conférences (secteurs variés : luxe, finance, santé, technologie, etc.)
25
+ - **Productions culturelles** : Spectacles, concerts, projections et performances artistiques
26
+ - **Espace Exposant** : Location d’espaces pour salons et événements d’entreprise
27
+ - **Boutique** : Librairie et boutique proposant des catalogues d’expositions, objets dérivés et éditions limitées
28
+
29
+ ## Accessibilité
30
+ - **Mobilité réduite** : Accès PMR aux espaces publics, ascenseurs et toilettes adaptées
31
+ - **Transports adaptés** : Arrêts de bus accessibles (ligne 6), parking à proximité avec places réservées
32
+ - **Services** : Personnel formé à l’accueil des personnes en situation de handicap
33
+ - **Signalétique** : Panneaux en braille et parcours tactiles pour les malvoyants (sur demande)
34
+
35
+ ## Tags agent
36
+ `monaco` `culture` `congrès` `expositions` `événementiel` `art` `mobilité douce` `écoresponsable` `centre culturel` `billetterie en ligne` `accessibilité PMR` `front de mer` `tourisme` `patrimoine
culture_data/MEDIATHEQUE_MONACO.md ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Médiathèque de Monaco
2
+
3
+ <!-- TYPE: lieu_statique -->
4
+ <!-- CATEGORY: culture -->
5
+ <!-- LAST_UPDATED: 2026-02-28 -->
6
+
7
+ ## Présentation
8
+ La Médiathèque Caroline est le nouveau pôle culturel et médiatique de la Mairie de Monaco, situé au 5 Promenade Honoré II. Conçue comme un espace moderne et accessible, elle est dédiée à la lecture, à l'apprentissage et à la découverte numérique pour tous les âges. Elle propose une vaste gamme de ressources médiatiques, d'ateliers et de services publics dans un environnement repensé pour le confort et l'accessibilité.
9
+
10
+ ## Informations pratiques
11
+ | Info | Détail |
12
+ |------|--------|
13
+ | **Adresse** | 5 Promenade Honoré II, 98000 Monaco |
14
+ | **Horaires** | Réouverture prévue le 11 décembre 2025 (horaires non précisés) |
15
+ | **Tarifs** | Gratuit (accès libre) |
16
+ | **Site web** | [www.mediatheque.mc](https://www.mediatheque.mc/) |
17
+ | **Téléphone** | +377 93 15 29 40 |
18
+ | **Accès** | Proche des transports en commun, parking à proximité |
19
+
20
+ ## Collections permanentes / Programmation
21
+ - **Livres** : Romans, essais, bandes dessinées, mangas, livres jeunesse
22
+ - **Médias numériques** : CD, DVD, livres audio, ressources en ligne
23
+ - **Presse** : Abonnements à des journaux et magazines locaux et internationaux
24
+ - **Ateliers et animations** :
25
+ - Ateliers d'écriture et de poésie
26
+ - Séances de musique et éveil musical pour enfants
27
+ - Rencontres littéraires et dédicaces
28
+ - Clubs de lecture et discussions thématiques
29
+ - Ciné-club et projections
30
+ - Ateliers parent-enfant (jardinage, yoga, éveil sensoriel)
31
+ - Expositions temporaires (littérature, arts visuels)
32
+ - **Espace jeunesse** : Activités adaptées aux enfants et adolescents
33
+
34
+ ## Accessibilité
35
+ - **Accès PMR** : Espace entièrement accessible aux personnes à mobilité réduite
36
+ - **Équipements adaptés** : Postes de travail ergonomiques, signalétique claire
37
+ - **Services spécifiques** : Aide à la recherche documentaire sur demande
38
+
39
+ ## Tags agent
40
+ `bibliothèque` `médiathèque` `lecture` `culture` `Monaco` `ateliers` `jeunesse` `numérique` `accessibilité` `événements` `livres` `mangas` `expositions
culture_data/MUSEE_OCEANO.md ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Musée Océanographique de Monaco
2
+
3
+ <!-- TYPE: lieu_statique -->
4
+ <!-- CATEGORY: culture, science, famille -->
5
+ <!-- LAST_UPDATED: 2026-02-28 -->
6
+
7
+ ## Présentation
8
+ Temple de la mer depuis 1910, le Musée Océanographique de Monaco allie architecture marine et engagement pour la protection des océans. Avec ses 6 500 m² dédiés à la découverte, il propose une immersion unique dans les écosystèmes marins, mêlant science, art et conservation. Ce lieu emblématique, fondé par le Prince Albert Ier, attire plus de 650 000 visiteurs par an.
9
+
10
+ ## Informations pratiques
11
+ | Info | Détail |
12
+ |------|--------|
13
+ | **Adresse** | Avenue Saint-Martin, Monaco-Ville, Monaco |
14
+ | **Horaires** | Octobre à mars : 10h-18h<br>Avril, mai, juin, septembre : 10h-19h<br>Juillet et août : 9h30-20h |
15
+ | **Tarifs** | Adulte : 22,50€<br>Étudiant : 14€<br>Enfant (4-17 ans) : 14€<br>Enfant (-4 ans) : Gratuit<br>Personne en situation de handicap : 11€<br>*Tarifs réduits pendant les Journées Européennes du Patrimoine* |
16
+ | **Site web** | [www.oceano.mc](https://www.oceano.mc) |
17
+ | **Téléphone** | +377 93 15 36 00 |
18
+ | **Accès** | Accès PMR par les caisses extérieures<br>Bus : lignes 1, 2 (arrêt "Oceanographic Museum")<br>Parking : Parking des Pêcheurs (à proximité) |
19
+
20
+ ## Collections permanentes / Programmation
21
+ - **Aquariums** :
22
+ - Lagon aux requins
23
+ - Espace tortues marines
24
+ - Aquariums méditerranéens et tropicaux
25
+ - Pouponnière de l’Institut Océanographique (visite *backstage*)
26
+ - **Expositions permanentes** :
27
+ - *Monaco et l’Océan* : engagement des Princes de Monaco pour la préservation marine
28
+ - *Oceanomania* : cabinet de curiosités par Mark Dion (fossiles, instruments anciens, maquettes)
29
+ - *Le Prince et la Méditerranée* : hommage au Prince Rainier III et au Commandant Cousteau
30
+ - **Expériences immersives** :
31
+ - Salle *Immersion* (650 m² d’écrans) : spectacles audiovisuels sur les écosystèmes (Arctique, Grande Barrière de Corail, Méditerranée)
32
+ - *ImmerSEAve VR* : réalité virtuelle dans l’Aire Marine Protégée
33
+ - **Animations** :
34
+ - *Animaux du bord de mer* (week-ends et vacances scolaires)
35
+ - Escape game thématique (30 min ou 1h)
36
+ - Visites guidées (sur réservation)
37
+ - **Toit-terrasse** :
38
+ - Restaurant *La Terrasse* avec vue panoramique sur la Méditerranée
39
+
40
+ ## Accessibilité
41
+ - **Personnes à mobilité réduite** : Accès dédié près des caisses extérieures, ascenseurs disponibles dans le musée.
42
+ - **Personnes malvoyantes** : Visites tactiles et audioguides adaptés (sur demande).
43
+ - **Personnes malentendantes** : Boucles magnétiques disponibles à l’accueil.
44
+ - **Fauteuils roulants** : Prêt gratuit sur réservation (nombre limité).
45
+ - **Chiens guides** : Autorisés dans l’ensemble du musée.
46
+
47
+ ## Tags agent
48
+ `monaco` `musée` `océanographie` `aquarium` `science` `nature` `art` `immersion` `famille` `accessible` `patrimoine` `écologie` `tortues` `requins` `réalité virtuelle` `expositions temporaires` `visite guidée` `vue panoramique` `Prince Albert Ier` `Commandant Cousteau
culture_data/THEATRE_MUSES.md ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Théâtre des Muses Monaco
2
+
3
+ <!-- TYPE: lieu_statique -->
4
+ <!-- CATEGORY: culture -->
5
+ <!-- LAST_UPDATED: 2026-02-28 -->
6
+
7
+ ## Présentation
8
+ Fondé en 2012 par Anthéa Sogno, le Théâtre des Muses est un lieu culturel intimiste de Monaco qui propose une programmation éclectique, alliant classiques et créations contemporaines. Ce théâtre à l'atmosphère chaleureuse et conviviale met l'accent sur le partage entre artistes et public, avec des moments d'échange après chaque représentation.
9
+
10
+ ## Informations pratiques
11
+ | Info | Détail |
12
+ |------|--------|
13
+ | **Adresse** | Non précisée (à vérifier sur le site officiel) |
14
+ | **Horaires** | Lundi et mardi : 15h-19h<br>Mercredi à vendredi : 10h-12h / 14h-18h<br>Samedi : 15h-18h<br>Dimanche : 15h-16h30<br>*Fermé les samedis/dimanches sans spectacles et pendant les vacances scolaires* |
15
+ | **Tarifs** | Spectacles adultes/tout public :<br>- Plein tarif : 28€<br>- Tarif réduit : 25€<br>- Jeunes (7-18 ans) et étudiants : 18€<br>Spectacles enfants : 15€ (tarif unique)<br>*Tarifs préférentiels pour comités d'entreprise et abonnements* |
16
+ | **Site web** | [www.letheatredesmuses.com](https://www.letheatredesmuses.com/) |
17
+ | **Téléphone** | Non précisé (à vérifier sur le site officiel) |
18
+ | **Accès** | Bus : Ligne 2 (Jardin Exotique) ou Ligne 3 (Hector Otto) - Arrêt Moneghetti<br>Gare SNCF : 5 min à pied<br>Parkings : Jardin Exotique ou Bosio (100m) - 0,30€/h après 20h |
19
+
20
+ ## Collections permanentes / Programmation
21
+ - **Classiques du répertoire** : Molière, Victor Hugo, Shakespeare, Sacha Guitry, Flaubert, Edmond Rostand, Marcel Pagnol
22
+ - **Créations contemporaines** : Pièces engagées, comédies légères, spectacles musicaux, créations poétiques
23
+ - **Spectacles pour enfants** : Programmation adaptée aux jeunes publics
24
+ - **Cours et stages** : Théâtre à partir de 6 ans, stages pour adultes et enfants
25
+ - **Événements privés** : Soirées, anniversaires, séminaires, conférences, projections de films
26
+ - **Artistes de renom** : Philippe Caubère, Éric Métayer, Michaël Lonsdale, Alexis Michalik
27
+ - **Spectacles à succès** : *Adieu Monsieur Haffmann*, *Les Chatouilles*, *Le Premier Homme*, *Le Barbier de Séville*
28
+
29
+ ## Accessibilité
30
+ Informations non précisées dans les sources. Il est recommandé de contacter le théâtre pour connaître les aménagements spécifiques (accès PMR, places adaptées, etc.).
31
+
32
+ ## Tags agent
33
+ `théâtre` `culture` `Monaco` `spectacle` `classiques` `contemporain` `enfants` `cours` `événements` `intimiste` `programmation` `artistes` `convivialité` `Avignon OFF` `moliere` `shakespeare
culture_data/THEATRE_PRINCESSE_GRACE.md ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Théâtre Princesse Grace Monaco
2
+
3
+ <!-- TYPE: lieu_statique -->
4
+ <!-- CATEGORY: culture -->
5
+ <!-- LAST_UPDATED: 2026-02-28 -->
6
+
7
+ ## Présentation
8
+ Le Théâtre Princesse Grace Monaco est une institution culturelle majeure de la Principauté, proposant une programmation variée de spectacles, concerts et événements artistiques. Situé au cœur de Monaco, il accueille des productions de qualité pour tous les publics.
9
+
10
+ ## Informations pratiques
11
+ | Info | Détail |
12
+ |------|--------|
13
+ | Adresse | 12, avenue d'Ostende, Monaco |
14
+ | Horaires | Du lundi au vendredi de 9h30 à 13h et de 14h à 17h |
15
+ | Tarifs | Variables selon les spectacles (consulter le site officiel) |
16
+ | Site web | [www.tpgmonaco.mc](https://www.tpgmonaco.mc) |
17
+ | Téléphone | +377 93 25 32 27 |
18
+ | Email | spectateurs@tpgmonaco.mc |
19
+ | Accès | Accessible en transports en commun et en voiture |
20
+
21
+ ## Collections permanentes / Programmation
22
+ - Saison théâtrale annuelle (octobre à mai)
23
+ - Spectacles variés (théâtre, one-man-shows, concerts)
24
+ - Collaborations avec des institutions culturelles locales
25
+ - Événements spéciaux et festivals
26
+
27
+ ## Accessibilité
28
+ Le théâtre est accessible aux personnes à mobilité réduite (PMR).
29
+
30
+ ## Tags agent
31
+ `théâtre` `culture` `monaco` `spectacle` `concert` `événement` `PMR` `art` `programmation
culture_data/events_cinema_monaco.json ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "id": "cinema_monaco_jumpers_2026-03-01",
4
+ "venue_id": "cinema_monaco",
5
+ "venue_name": "Cinémas de Monaco",
6
+ "title": "Jumpers",
7
+ "description": "Salle 1 : 14h00",
8
+ "date_start": "2026-03-01",
9
+ "date_end": null,
10
+ "always_current": true,
11
+ "start_time": "14:00",
12
+ "end_time": null,
13
+ "tags": [
14
+ "Animation",
15
+ "Aventure",
16
+ "Comédie",
17
+ "Projection de film"
18
+ ],
19
+ "price": null,
20
+ "price_value": 0,
21
+ "free": false,
22
+ "url": "https://www.cinemas2monaco.com/index.php?page=12&film=859",
23
+ "image_url": "https://www.cinemas2monaco.com./uploadz/affiches/BJ6EA081EXG3KLSONKUCSCSY310H1T.jpg",
24
+ "source": "https://www.cinemas2monaco.com/index.php",
25
+ "scraped_at": "2026-03-01T11:57:07Z"
26
+ },
27
+ {
28
+ "id": "cinema_monaco_chers-parents_2026-03-01",
29
+ "venue_id": "cinema_monaco",
30
+ "venue_name": "Cinémas de Monaco",
31
+ "title": "Chers parents",
32
+ "description": "Salle 1 : 11h00, 14h00, 16h15, 18h30 | Salle 2 : 20h50",
33
+ "date_start": "2026-03-01",
34
+ "date_end": null,
35
+ "always_current": true,
36
+ "start_time": "11:00",
37
+ "end_time": null,
38
+ "tags": [
39
+ "Comédie",
40
+ "Projection de film"
41
+ ],
42
+ "price": null,
43
+ "price_value": 0,
44
+ "free": false,
45
+ "url": "https://www.cinemas2monaco.com/index.php?page=12&film=862",
46
+ "image_url": "https://www.cinemas2monaco.com./uploadz/affiches/R7JZVWP01MG306EFC8HYQU9PZ99DBH.jpg",
47
+ "source": "https://www.cinemas2monaco.com/index.php",
48
+ "scraped_at": "2026-03-01T11:57:07Z"
49
+ },
50
+ {
51
+ "id": "cinema_monaco_les-enfants-de-la-resistance_2026-03-01",
52
+ "venue_id": "cinema_monaco",
53
+ "venue_name": "Cinémas de Monaco",
54
+ "title": "Les Enfants de la Résistance",
55
+ "description": "Salle 1 : 11h00, 14h00, 18h30 | Salle 2 : 14h00",
56
+ "date_start": "2026-03-01",
57
+ "date_end": null,
58
+ "always_current": true,
59
+ "start_time": "11:00",
60
+ "end_time": null,
61
+ "tags": [
62
+ "Aventure",
63
+ "Drame",
64
+ "Famille",
65
+ "Historique",
66
+ "Projection de film"
67
+ ],
68
+ "price": null,
69
+ "price_value": 0,
70
+ "free": false,
71
+ "url": "https://www.cinemas2monaco.com/index.php?page=12&film=863",
72
+ "image_url": "https://www.cinemas2monaco.com./uploadz/affiches/UHNKR604PZFEB37FKQQBO8LL2QB6PH.jpg",
73
+ "source": "https://www.cinemas2monaco.com/index.php",
74
+ "scraped_at": "2026-03-01T11:57:07Z"
75
+ },
76
+ {
77
+ "id": "cinema_monaco_le-reve-americain_2026-03-01",
78
+ "venue_id": "cinema_monaco",
79
+ "venue_name": "Cinémas de Monaco",
80
+ "title": "Le Rêve américain",
81
+ "description": "Salle 2 : 13h45, 14h00, 18h30, 20h50",
82
+ "date_start": "2026-03-01",
83
+ "date_end": null,
84
+ "always_current": true,
85
+ "start_time": "13:45",
86
+ "end_time": null,
87
+ "tags": [
88
+ "Comédie",
89
+ "Projection de film"
90
+ ],
91
+ "price": null,
92
+ "price_value": 0,
93
+ "free": false,
94
+ "url": "https://www.cinemas2monaco.com/index.php?page=12&film=851",
95
+ "image_url": "https://www.cinemas2monaco.com./uploadz/affiches/GIH666RZNUI3KZC79D9074F784LPFH.jpg",
96
+ "source": "https://www.cinemas2monaco.com/index.php",
97
+ "scraped_at": "2026-03-01T11:57:07Z"
98
+ },
99
+ {
100
+ "id": "cinema_monaco_marty-supreme_2026-03-01",
101
+ "venue_id": "cinema_monaco",
102
+ "venue_name": "Cinémas de Monaco",
103
+ "title": "Marty Supreme",
104
+ "description": "Salle 1 : 11h00 (VF), 15h45 (VF), 18h00 (VO), 20h50 (VO)",
105
+ "date_start": "2026-03-01",
106
+ "date_end": null,
107
+ "always_current": true,
108
+ "start_time": "11:00",
109
+ "end_time": null,
110
+ "tags": [
111
+ "Biopic",
112
+ "Drame",
113
+ "Projection de film"
114
+ ],
115
+ "price": null,
116
+ "price_value": 0,
117
+ "free": false,
118
+ "url": "https://www.cinemas2monaco.com/index.php?page=12&film=856",
119
+ "image_url": "https://www.cinemas2monaco.com./uploadz/affiches/BY69FMOCXJXUOLTEB5OC44MSWGXBID.jpg",
120
+ "source": "https://www.cinemas2monaco.com/index.php",
121
+ "scraped_at": "2026-03-01T11:57:07Z"
122
+ },
123
+ {
124
+ "id": "cinema_monaco_goat-rever-plus-haut_2026-03-01",
125
+ "venue_id": "cinema_monaco",
126
+ "venue_name": "Cinémas de Monaco",
127
+ "title": "GOAT - rêver plus haut",
128
+ "description": "Salle 2 : 16h15",
129
+ "date_start": "2026-03-01",
130
+ "date_end": null,
131
+ "always_current": true,
132
+ "start_time": "16:15",
133
+ "end_time": null,
134
+ "tags": [
135
+ "Action",
136
+ "Animation",
137
+ "Comédie",
138
+ "Projection de film"
139
+ ],
140
+ "price": null,
141
+ "price_value": 0,
142
+ "free": false,
143
+ "url": "https://www.cinemas2monaco.com/index.php?page=12&film=853",
144
+ "image_url": "https://www.cinemas2monaco.com./uploadz/affiches/EJ8TDZFT71ACTQRWHJCSRVDZCGVLL6.jpg",
145
+ "source": "https://www.cinemas2monaco.com/index.php",
146
+ "scraped_at": "2026-03-01T11:57:07Z"
147
+ },
148
+ {
149
+ "id": "cinema_monaco_marsupilami_2026-03-01",
150
+ "venue_id": "cinema_monaco",
151
+ "venue_name": "Cinémas de Monaco",
152
+ "title": "Marsupilami",
153
+ "description": "Salle 1 : 16h15 | Salle 2 : 16h00, 16h15",
154
+ "date_start": "2026-03-01",
155
+ "date_end": null,
156
+ "always_current": true,
157
+ "start_time": "16:00",
158
+ "end_time": null,
159
+ "tags": [
160
+ "Aventure",
161
+ "Comédie",
162
+ "Projection de film"
163
+ ],
164
+ "price": null,
165
+ "price_value": 0,
166
+ "free": false,
167
+ "url": "https://www.cinemas2monaco.com/index.php?page=12&film=847",
168
+ "image_url": "https://www.cinemas2monaco.com./uploadz/affiches/NAX17BAX01CSLHWKCY2O6OL31ZXDKG.jpg",
169
+ "source": "https://www.cinemas2monaco.com/index.php",
170
+ "scraped_at": "2026-03-01T11:57:07Z"
171
+ },
172
+ {
173
+ "id": "cinema_monaco_nuremberg_2026-03-01",
174
+ "venue_id": "cinema_monaco",
175
+ "venue_name": "Cinémas de Monaco",
176
+ "title": "Nuremberg",
177
+ "description": "Salle 2 : 11h00 (VO), 18h00 (VO)",
178
+ "date_start": "2026-03-01",
179
+ "date_end": null,
180
+ "always_current": true,
181
+ "start_time": "11:00",
182
+ "end_time": null,
183
+ "tags": [
184
+ "Drame",
185
+ "Historique",
186
+ "Projection de film"
187
+ ],
188
+ "price": null,
189
+ "price_value": 0,
190
+ "free": false,
191
+ "url": "https://www.cinemas2monaco.com/index.php?page=12&film=857",
192
+ "image_url": "https://www.cinemas2monaco.com./uploadz/affiches/71JIXICO14HBUIEO3COJDB105EKY19.jpg",
193
+ "source": "https://www.cinemas2monaco.com/index.php",
194
+ "scraped_at": "2026-03-01T11:57:07Z"
195
+ },
196
+ {
197
+ "id": "cinema_monaco_gourou_2026-03-01",
198
+ "venue_id": "cinema_monaco",
199
+ "venue_name": "Cinémas de Monaco",
200
+ "title": "Gourou",
201
+ "description": "Salle 2 : 11h00, 18h30",
202
+ "date_start": "2026-03-01",
203
+ "date_end": null,
204
+ "always_current": true,
205
+ "start_time": "11:00",
206
+ "end_time": null,
207
+ "tags": [
208
+ "Drame",
209
+ "Thriller",
210
+ "Projection de film"
211
+ ],
212
+ "price": null,
213
+ "price_value": 0,
214
+ "free": false,
215
+ "url": "https://www.cinemas2monaco.com/index.php?page=12&film=852",
216
+ "image_url": "https://www.cinemas2monaco.com./uploadz/affiches/X2FKOEX5ARDFLZY5VPL12TTC2YGLYN.jpg",
217
+ "source": "https://www.cinemas2monaco.com/index.php",
218
+ "scraped_at": "2026-03-01T11:57:07Z"
219
+ },
220
+ {
221
+ "id": "cinema_monaco_christy_2026-03-01",
222
+ "venue_id": "cinema_monaco",
223
+ "venue_name": "Cinémas de Monaco",
224
+ "title": "Christy",
225
+ "description": "Horaires non précisés pour le 04/03/2026",
226
+ "date_start": "2026-03-01",
227
+ "date_end": null,
228
+ "always_current": true,
229
+ "start_time": null,
230
+ "end_time": null,
231
+ "tags": [
232
+ "Projection de film"
233
+ ],
234
+ "price": null,
235
+ "price_value": 0,
236
+ "free": false,
237
+ "url": "https://www.cinemas2monaco.com/index.php?page=12&film=858",
238
+ "image_url": "https://www.cinemas2monaco.com./uploadz/affiches/71JIXICO14HBUIEO3COJDB105EKY19.jpg",
239
+ "source": "https://www.cinemas2monaco.com/index.php",
240
+ "scraped_at": "2026-03-01T11:57:07Z"
241
+ }
242
+ ]
culture_data/events_grimaldi_forum.json ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "id": "grimaldi_forum_issa-doumbia-les-serenissimes-de-l-humour_2026-03-10",
4
+ "venue_id": "grimaldi_forum",
5
+ "venue_name": "Grimaldi Forum Monaco",
6
+ "title": "ISSA DOUMBIA - LES SÉRÉNISSIMES DE L'HUMOUR",
7
+ "description": "Spectacle d'humour avec Issa Doumbia dans le cadre des Sérénissimes de l'Humour.",
8
+ "date_start": "2026-03-10",
9
+ "date_end": "2026-03-10",
10
+ "always_current": false,
11
+ "start_time": "20:00",
12
+ "end_time": null,
13
+ "tags": [
14
+ "humour",
15
+ "spectacle"
16
+ ],
17
+ "price": "À partir de 35€",
18
+ "price_value": 35,
19
+ "free": false,
20
+ "url": "https://www.grimaldiforum.com/en/events-schedule-monaco/issa-doumbia-les-serenissimes-de-l-humour",
21
+ "image_url": "/medias/c/465x254/content/AGENDA/2026/1ER SEMESTRE 2026/MICE/SH/photo-presse-1-c-houtkauv.jpg?1759839195",
22
+ "source": "https://www.grimaldiforum.com/agenda",
23
+ "scraped_at": "2026-02-28T15:55:02Z"
24
+ },
25
+ {
26
+ "id": "grimaldi_forum_dany-boon-les-serenissimes-de-l-humour_2026-03-11",
27
+ "venue_id": "grimaldi_forum",
28
+ "venue_name": "Grimaldi Forum Monaco",
29
+ "title": "DANY BOON - LES SÉRÉNISSIMES DE L'HUMOUR",
30
+ "description": "Spectacle d'humour avec Dany Boon dans le cadre des Sérénissimes de l'Humour.",
31
+ "date_start": "2026-03-11",
32
+ "date_end": "2026-03-11",
33
+ "always_current": false,
34
+ "start_time": "20:00",
35
+ "end_time": null,
36
+ "tags": [
37
+ "humour",
38
+ "spectacle"
39
+ ],
40
+ "price": "À partir de 45€",
41
+ "price_value": 45,
42
+ "free": false,
43
+ "url": "https://www.grimaldiforum.com/en/events-schedule-monaco/dany-boon-les-serenissimes-de-l-humour",
44
+ "image_url": "/medias/c/465x254/content/AGENDA/2026/1ER SEMESTRE 2026/MICE/SH/kobayashi_photos-danyboon-1666.jpg?1759839540",
45
+ "source": "https://www.grimaldiforum.com/agenda",
46
+ "scraped_at": "2026-02-28T15:55:02Z"
47
+ },
48
+ {
49
+ "id": "grimaldi_forum_nawell-madani-les-serenissimes-de-l-humour_2026-03-12",
50
+ "venue_id": "grimaldi_forum",
51
+ "venue_name": "Grimaldi Forum Monaco",
52
+ "title": "NAWELL MADANI - LES SÉRÉNISSIMES DE L'HUMOUR",
53
+ "description": "Spectacle d'humour avec Nawell Madani dans le cadre des Sérénissimes de l'Humour.",
54
+ "date_start": "2026-03-12",
55
+ "date_end": "2026-03-12",
56
+ "always_current": false,
57
+ "start_time": "20:00",
58
+ "end_time": null,
59
+ "tags": [
60
+ "humour",
61
+ "spectacle"
62
+ ],
63
+ "price": "À partir de 35€",
64
+ "price_value": 35,
65
+ "free": false,
66
+ "url": "https://www.grimaldiforum.com/en/events-schedule-monaco/nawaell-madani-les-serenissimes-de-l-humour",
67
+ "image_url": "/medias/c/465x254/content/AGENDA/2026/1ER SEMESTRE 2026/MICE/SH/nawell-madani-c-jean-baptiste-mondino-3.jpg?1759839848",
68
+ "source": "https://www.grimaldiforum.com/agenda",
69
+ "scraped_at": "2026-02-28T15:55:02Z"
70
+ },
71
+ {
72
+ "id": "grimaldi_forum_le-comedy-les-serenissimes-de-l-humour_2026-03-13",
73
+ "venue_id": "grimaldi_forum",
74
+ "venue_name": "Grimaldi Forum Monaco",
75
+ "title": "LE COMEDY - LES SÉRÉNISSIMES DE L'HUMOUR",
76
+ "description": "Spectacle d'humour avec plusieurs artistes dans le cadre des Sérénissimes de l'Humour.",
77
+ "date_start": "2026-03-13",
78
+ "date_end": "2026-03-13",
79
+ "always_current": false,
80
+ "start_time": "20:00",
81
+ "end_time": null,
82
+ "tags": [
83
+ "humour",
84
+ "spectacle"
85
+ ],
86
+ "price": "À partir de 30€",
87
+ "price_value": 30,
88
+ "free": false,
89
+ "url": "https://www.grimaldiforum.com/en/events-schedule-monaco/le-comedy-les-serenissimes-de-l-humour",
90
+ "image_url": "/medias/c/465x254/content/AGENDA/2026/1ER SEMESTRE 2026/MICE/SH/plateau.png?1759840292",
91
+ "source": "https://www.grimaldiforum.com/agenda",
92
+ "scraped_at": "2026-02-28T15:55:02Z"
93
+ },
94
+ {
95
+ "id": "grimaldi_forum_noelle-perna-les-serenissimes-de-l-humour_2026-03-14",
96
+ "venue_id": "grimaldi_forum",
97
+ "venue_name": "Grimaldi Forum Monaco",
98
+ "title": "NOELLE PERNA - LES SÉRÉNISSIMES DE L'HUMOUR",
99
+ "description": "Spectacle d'humour avec Noelle Perna dans le cadre des Sérénissimes de l'Humour.",
100
+ "date_start": "2026-03-14",
101
+ "date_end": "2026-03-14",
102
+ "always_current": false,
103
+ "start_time": "20:00",
104
+ "end_time": null,
105
+ "tags": [
106
+ "humour",
107
+ "spectacle"
108
+ ],
109
+ "price": "À partir de 35€",
110
+ "price_value": 35,
111
+ "free": false,
112
+ "url": "https://www.grimaldiforum.com/en/events-schedule-monaco/noelle-perna-les-serenissimes-de-l-humour",
113
+ "image_url": "/medias/c/465x254/content/AGENDA/2026/1ER SEMESTRE 2026/MICE/SH/NoellePerna.jpeg?1759840676",
114
+ "source": "https://www.grimaldiforum.com/agenda",
115
+ "scraped_at": "2026-02-28T15:55:02Z"
116
+ },
117
+ {
118
+ "id": "grimaldi_forum_stand-up-monaco_2026-03-17",
119
+ "venue_id": "grimaldi_forum",
120
+ "venue_name": "Grimaldi Forum Monaco",
121
+ "title": "STAND UP MONACO",
122
+ "description": "Soirée d'humour stand-up avec plusieurs artistes.",
123
+ "date_start": "2026-03-17",
124
+ "date_end": "2026-03-17",
125
+ "always_current": false,
126
+ "start_time": "19:00",
127
+ "end_time": null,
128
+ "tags": [
129
+ "humour",
130
+ "stand-up"
131
+ ],
132
+ "price": "À partir de 25€",
133
+ "price_value": 25,
134
+ "free": false,
135
+ "url": "https://www.grimaldiforum.com/en/events-schedule-monaco/stand-up-monaco-3",
136
+ "image_url": "/medias/c/465x254/content/AGENDA/2026/1ER SEMESTRE 2026/stand-up_monaco-header-evenement-site-web-l1920-x-h657-px-260213.jpg?1771422815",
137
+ "source": "https://www.grimaldiforum.com/agenda",
138
+ "scraped_at": "2026-02-28T15:55:02Z"
139
+ },
140
+ {
141
+ "id": "grimaldi_forum_thursday-live-sessions-radical-ronron_2026-03-19",
142
+ "venue_id": "grimaldi_forum",
143
+ "venue_name": "Grimaldi Forum Monaco",
144
+ "title": "THURSDAY LIVE SESSIONS - RADICAL RONRON",
145
+ "description": "Concert live dans le cadre des Thursday Live Sessions avec Radical Ronron.",
146
+ "date_start": "2026-03-19",
147
+ "date_end": "2026-03-19",
148
+ "always_current": false,
149
+ "start_time": "18:30",
150
+ "end_time": null,
151
+ "tags": [
152
+ "concert",
153
+ "live",
154
+ "musique"
155
+ ],
156
+ "price": null,
157
+ "price_value": 0,
158
+ "free": true,
159
+ "url": "https://www.grimaldiforum.com/en/events-schedule-monaco/thursday-live-sessions-radical-ronron",
160
+ "image_url": "/medias/c/465x254/content/AGENDA/2026/1ER SEMESTRE 2026/tls-radical-ronron_photos-fb-2.png?1759148876",
161
+ "source": "https://www.grimaldiforum.com/agenda",
162
+ "scraped_at": "2026-02-28T15:55:02Z"
163
+ },
164
+ {
165
+ "id": "grimaldi_forum_david-hallyday_2026-03-20",
166
+ "venue_id": "grimaldi_forum",
167
+ "venue_name": "Grimaldi Forum Monaco",
168
+ "title": "DAVID HALLYDAY",
169
+ "description": "Concert de David Hallyday.",
170
+ "date_start": "2026-03-20",
171
+ "date_end": "2026-03-20",
172
+ "always_current": false,
173
+ "start_time": "20:30",
174
+ "end_time": null,
175
+ "tags": [
176
+ "concert",
177
+ "musique"
178
+ ],
179
+ "price": "À partir de 45€",
180
+ "price_value": 45,
181
+ "free": false,
182
+ "url": "https://www.grimaldiforum.com/en/events-schedule-monaco/david-hallyday-en-concert",
183
+ "image_url": "/medias/c/465x254/content/AGENDA/2026/1ER SEMESTRE 2026/img_3100.jpeg?1754394503",
184
+ "source": "https://www.grimaldiforum.com/agenda",
185
+ "scraped_at": "2026-02-28T15:55:02Z"
186
+ },
187
+ {
188
+ "id": "grimaldi_forum_jeremy-frerot-en-concert_2026-05-08",
189
+ "venue_id": "grimaldi_forum",
190
+ "venue_name": "Grimaldi Forum Monaco",
191
+ "title": "JÉRÉMY FREROT EN CONCERT",
192
+ "description": "Concert de Jérémy Frerot.",
193
+ "date_start": "2026-05-08",
194
+ "date_end": "2026-05-08",
195
+ "always_current": false,
196
+ "start_time": "20:30",
197
+ "end_time": null,
198
+ "tags": [
199
+ "concert",
200
+ "musique"
201
+ ],
202
+ "price": "À partir de 29€",
203
+ "price_value": 29,
204
+ "free": false,
205
+ "url": "https://www.grimaldiforum.com/fr/agenda-manifestations-monaco/jeremy-frerot-en-concert",
206
+ "image_url": "/medias/c/779x627/content/AGENDA/2026/1ER SEMESTRE 2026/jeremyfrerot_portrait-day2-guadeloupe_tl_05331-2.jpg?1767802484",
207
+ "source": "https://www.grimaldiforum.com/agenda",
208
+ "scraped_at": "2026-02-28T15:55:02Z"
209
+ },
210
+ {
211
+ "id": "grimaldi_forum_la-legende-de-monte-cristo-le-musical_2026-05-09",
212
+ "venue_id": "grimaldi_forum",
213
+ "venue_name": "Grimaldi Forum Monaco",
214
+ "title": "LA LÉGENDE DE MONTE-CRISTO - LE MUSICAL",
215
+ "description": "Comédie musicale basée sur l'œuvre d'Alexandre Dumas.",
216
+ "date_start": "2026-05-09",
217
+ "date_end": "2026-05-10",
218
+ "always_current": false,
219
+ "start_time": "20:30",
220
+ "end_time": null,
221
+ "tags": [
222
+ "comédie musicale",
223
+ "spectacle"
224
+ ],
225
+ "price": "À partir de 39€",
226
+ "price_value": 39,
227
+ "free": false,
228
+ "url": "https://www.grimaldiforum.com/fr/agenda-manifestations-monaco/la-legende-de-monte-cristo-le-musical",
229
+ "image_url": "/medias/c/779x627/content/AGENDA/2026/1ER SEMESTRE 2026/lldmc-digital-2660x1140-banie-re-spotify.jpg?1767188377",
230
+ "source": "https://www.grimaldiforum.com/agenda",
231
+ "scraped_at": "2026-02-28T15:55:02Z"
232
+ },
233
+ {
234
+ "id": "grimaldi_forum_monaco-et-l-automobile-de-1893-a-nos-jours_2026-07-01",
235
+ "venue_id": "grimaldi_forum",
236
+ "venue_name": "Grimaldi Forum Monaco",
237
+ "title": "MONACO ET L'AUTOMOBILE, DE 1893 À NOS JOURS",
238
+ "description": "Exposition retraçant plus de 130 ans d'histoire automobile en Principauté.",
239
+ "date_start": "2026-07-01",
240
+ "date_end": "2026-09-06",
241
+ "always_current": false,
242
+ "start_time": "10:00",
243
+ "end_time": "22:00",
244
+ "tags": [
245
+ "exposition",
246
+ "histoire",
247
+ "automobile"
248
+ ],
249
+ "price": "À partir de 7,50€ (prévente)",
250
+ "price_value": 7,
251
+ "free": false,
252
+ "url": "https://www.grimaldiforum.com/fr/agenda-manifestations-monaco/monaco-et-l-automobile-de-1893-a-nos-jours",
253
+ "image_url": "/medias/c/779x627/content/AGENDA/2026/1ER SEMESTRE 2026/EXPO AUTO/gfm-auto-a4-pre-vente-2512102.jpg?1770656087",
254
+ "source": "https://www.grimaldiforum.com/agenda",
255
+ "scraped_at": "2026-02-28T15:55:02Z"
256
+ },
257
+ {
258
+ "id": "grimaldi_forum_shrek-the-musical_2026-12-01",
259
+ "venue_id": "grimaldi_forum",
260
+ "venue_name": "Grimaldi Forum Monaco",
261
+ "title": "SHREK THE MUSICAL",
262
+ "description": "Comédie musicale basée sur le film d'animation Shrek.",
263
+ "date_start": "2026-12-01",
264
+ "date_end": "2026-12-31",
265
+ "always_current": false,
266
+ "start_time": null,
267
+ "end_time": null,
268
+ "tags": [
269
+ "comédie musicale",
270
+ "spectacle"
271
+ ],
272
+ "price": null,
273
+ "price_value": 0,
274
+ "free": false,
275
+ "url": null,
276
+ "image_url": null,
277
+ "source": "https://monacolife.net/grimaldi-forum-extension-pays-dividends-as-historic-2026-calendar-is-revealed/",
278
+ "scraped_at": "2026-02-28T15:55:02Z"
279
+ }
280
+ ]
culture_data/events_mediatheque_monaco.json ADDED
@@ -0,0 +1,289 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "id": "mediatheque_monaco_printemps-des-poetes-flow-d-ados-autour-du-slam-et-du-rap-avec-yass-sogo_2026-02-26",
4
+ "venue_id": "mediatheque_monaco",
5
+ "venue_name": "Médiathèque de Monaco",
6
+ "title": "Printemps des Poètes - Flow d’ados autour du Slam et du Rap avec Yass Sogo",
7
+ "description": "À l’occasion du Printemps des Poètes, un atelier slam est proposé pour explorer la puissance des mots parlés et la musicalité de la langue. Encadrés par Yass Sogo, rappeur et professeur de français, les participants apprendront à écrire, dire et ressentir le slam.",
8
+ "date_start": "2026-02-26",
9
+ "date_end": "2026-02-26",
10
+ "always_current": false,
11
+ "start_time": "14:00",
12
+ "end_time": "17:00",
13
+ "tags": [
14
+ "Poésie",
15
+ "Atelier",
16
+ "Slam",
17
+ "Rap"
18
+ ],
19
+ "price": null,
20
+ "price_value": 0,
21
+ "free": true,
22
+ "url": "https://www.mediatheque.mc/Default/doc/CALENDAR/1645/printemps-des-poetes-flow-d-ados-autour-du-slam-et-du-rap-avec-yass-sogo",
23
+ "image_url": "https://www.mediatheque.mc/basicimagedownload.ashx?itemGuid=AF517258-7417-4110-8D7C-36AB71454D54",
24
+ "source": "https://www.mediatheque.mc/search.aspx?SC=CALENDAR_PLANNER_1",
25
+ "scraped_at": "2026-02-28T14:56:46Z"
26
+ },
27
+ {
28
+ "id": "mediatheque_monaco_printemps-des-poetes-flow-d-ados-autour-du-slam-et-du-rap-avec-yass-sogo_2026-02-27",
29
+ "venue_id": "mediatheque_monaco",
30
+ "venue_name": "Médiathèque de Monaco",
31
+ "title": "Printemps des Poètes - Flow d’ados autour du Slam et du Rap avec Yass Sogo",
32
+ "description": "À l’occasion du Printemps des Poètes, un atelier slam est proposé pour explorer la puissance des mots parlés et la musicalité de la langue. Encadrés par Yass Sogo, rappeur et professeur de français, les participants apprendront à écrire, dire et ressentir le slam.",
33
+ "date_start": "2026-02-27",
34
+ "date_end": "2026-02-27",
35
+ "always_current": false,
36
+ "start_time": "14:00",
37
+ "end_time": "17:00",
38
+ "tags": [
39
+ "Poésie",
40
+ "Atelier",
41
+ "Slam",
42
+ "Rap"
43
+ ],
44
+ "price": null,
45
+ "price_value": 0,
46
+ "free": true,
47
+ "url": "https://www.mediatheque.mc/Default/doc/CALENDAR/1645/printemps-des-poetes-flow-d-ados-autour-du-slam-et-du-rap-avec-yass-sogo",
48
+ "image_url": "https://www.mediatheque.mc/basicimagedownload.ashx?itemGuid=AF517258-7417-4110-8D7C-36AB71454D54",
49
+ "source": "https://www.mediatheque.mc/search.aspx?SC=CALENDAR_PLANNER_1",
50
+ "scraped_at": "2026-02-28T14:56:46Z"
51
+ },
52
+ {
53
+ "id": "mediatheque_monaco_picnic-music-un-concert-sur-grand-ecran-pour-accompagner-votre-pause-dejeuner_2026-03-03",
54
+ "venue_id": "mediatheque_monaco",
55
+ "venue_name": "Médiathèque de Monaco",
56
+ "title": "Picnic Music - Un concert sur grand écran pour accompagner votre pause déjeuner",
57
+ "description": "Envie d’une pause déjeuner musicale ? Venez profiter de ce moment privilégié pour savourer votre repas, seul, entre collègues ou entre amis tout en regardant un concert projeté sur l’écran de l’Atrium de la Médiathèque Caroline. Une programmation variée.",
58
+ "date_start": "2026-03-03",
59
+ "date_end": "2026-03-03",
60
+ "always_current": false,
61
+ "start_time": "12:15",
62
+ "end_time": "13:45",
63
+ "tags": [
64
+ "Musique",
65
+ "Concert",
66
+ "Projection"
67
+ ],
68
+ "price": null,
69
+ "price_value": 0,
70
+ "free": true,
71
+ "url": "https://www.mediatheque.mc/Default/doc/CALENDAR/1686/picnic-music-un-concert-sur-grand-ecran-pour-accompagner-votre-pause-dejeuner",
72
+ "image_url": "https://www.mediatheque.mc/basicimagedownload.ashx?itemGuid=6E0B495F-D4F5-4E44-80EB-06FC113F2E53",
73
+ "source": "https://www.mediatheque.mc/search.aspx?SC=CALENDAR_PLANNER_1",
74
+ "scraped_at": "2026-02-28T14:56:46Z"
75
+ },
76
+ {
77
+ "id": "mediatheque_monaco_printemps-des-poetes-atelier-d-ecriture-poetique-avec-audrey-greninger-cesbron_2026-03-03",
78
+ "venue_id": "mediatheque_monaco",
79
+ "venue_name": "Médiathèque de Monaco",
80
+ "title": "Printemps des Poètes - Atelier d’écriture poétique avec Audrey Greninger Cesbron",
81
+ "description": "Dans le cadre du Printemps des Poètes, cet atelier invite les participants à jouer avec les mots et à laisser s’exprimer leur imagination. À travers des propositions ludiques et créatives, vous expérimenterez la poésie sous différentes formes : mots, vers, haïkus...",
82
+ "date_start": "2026-03-03",
83
+ "date_end": "2026-03-03",
84
+ "always_current": false,
85
+ "start_time": "14:30",
86
+ "end_time": "16:30",
87
+ "tags": [
88
+ "Poésie",
89
+ "Atelier"
90
+ ],
91
+ "price": null,
92
+ "price_value": 0,
93
+ "free": true,
94
+ "url": "https://www.mediatheque.mc/Default/doc/CALENDAR/1646/printemps-des-poetes-atelier-d-ecriture-poetique-avec-audrey-greninger-cesbron",
95
+ "image_url": "https://www.mediatheque.mc/basicimagedownload.ashx?itemGuid=F4060E7B-A22E-47CF-AC3C-37A2E44DDBDC",
96
+ "source": "https://www.mediatheque.mc/search.aspx?SC=CALENDAR_PLANNER_1",
97
+ "scraped_at": "2026-02-28T14:56:46Z"
98
+ },
99
+ {
100
+ "id": "mediatheque_monaco_atelier-parent-enfant-petite-plantation_2026-03-04",
101
+ "venue_id": "mediatheque_monaco",
102
+ "venue_name": "Médiathèque de Monaco",
103
+ "title": "Atelier parent-enfant - Petite plantation",
104
+ "description": "Petits jardiniers en herbe, soyez prêts à mettre la main dans la terre pour une petite plantation. Le printemps approche, il est temps de faire pousser les plus belles plantes. Toujours à 4 mains, cet atelier parent/enfant vous permettra de repartir avec votre création.",
105
+ "date_start": "2026-03-04",
106
+ "date_end": "2026-03-04",
107
+ "always_current": false,
108
+ "start_time": "15:00",
109
+ "end_time": "16:00",
110
+ "tags": [
111
+ "Jeunesse",
112
+ "Atelier"
113
+ ],
114
+ "price": null,
115
+ "price_value": 0,
116
+ "free": true,
117
+ "url": "https://www.mediatheque.mc/Default/doc/CALENDAR/1644/atelier-parent-enfant-petite-plantation",
118
+ "image_url": "https://www.mediatheque.mc/basicimagedownload.ashx?itemGuid=62AAB4DB-7804-4BD3-BC49-A3ADD5B7CB09",
119
+ "source": "https://www.mediatheque.mc/search.aspx?SC=CALENDAR_PLANNER_1",
120
+ "scraped_at": "2026-02-28T14:56:46Z"
121
+ },
122
+ {
123
+ "id": "mediatheque_monaco_exposition-sur-les-chemins-de-la-litterature-ado_2026-03-05",
124
+ "venue_id": "mediatheque_monaco",
125
+ "venue_name": "Médiathèque de Monaco",
126
+ "title": "Exposition - Sur les chemins de la littérature ado",
127
+ "description": "Du 5 au 31 mars, le public est invité à découvrir une exposition intitulée Sur les chemins de la littérature ado qui mettra en lumière les grandes thématiques, les voix marquantes et les univers qui font vibrer les jeunes lecteurs d’aujourd’hui.",
128
+ "date_start": "2026-03-05",
129
+ "date_end": "2026-03-31",
130
+ "always_current": false,
131
+ "start_time": "13:00",
132
+ "end_time": "18:30",
133
+ "tags": [
134
+ "Exposition",
135
+ "Littérature",
136
+ "Jeunesse"
137
+ ],
138
+ "price": null,
139
+ "price_value": 0,
140
+ "free": true,
141
+ "url": "https://www.mediatheque.mc/Default/doc/CALENDAR/1643/exposition-sur-les-chemins-de-la-litterature-ado",
142
+ "image_url": "https://www.mediatheque.mc/basicimagedownload.ashx?itemGuid=77A13DA8-35BD-4BB1-96F3-9D1EF1F44FCE",
143
+ "source": "https://www.mediatheque.mc/search.aspx?SC=CALENDAR_PLANNER_1",
144
+ "scraped_at": "2026-02-28T14:56:46Z"
145
+ },
146
+ {
147
+ "id": "mediatheque_monaco_eveil-musical_2026-03-06",
148
+ "venue_id": "mediatheque_monaco",
149
+ "venue_name": "Médiathèque de Monaco",
150
+ "title": "Eveil musical",
151
+ "description": "Chants, rythmes, sons et petits instruments invitent les tout-petits à écouter, bouger, explorer et s’exprimer à leur façon.",
152
+ "date_start": "2026-03-06",
153
+ "date_end": "2026-03-06",
154
+ "always_current": false,
155
+ "start_time": "10:30",
156
+ "end_time": "11:15",
157
+ "tags": [
158
+ "Tout-petits",
159
+ "Musique",
160
+ "Atelier"
161
+ ],
162
+ "price": null,
163
+ "price_value": 0,
164
+ "free": true,
165
+ "url": "https://www.mediatheque.mc/Default/doc/CALENDAR/1681/eveil-musical",
166
+ "image_url": "https://www.mediatheque.mc/basicimagedownload.ashx?itemGuid=3AB77114-12E3-48E4-80C7-989990362AB7",
167
+ "source": "https://www.mediatheque.mc/search.aspx?SC=CALENDAR_PLANNER_1",
168
+ "scraped_at": "2026-02-28T14:56:46Z"
169
+ },
170
+ {
171
+ "id": "mediatheque_monaco_yoga-en-famille_2026-03-07",
172
+ "venue_id": "mediatheque_monaco",
173
+ "venue_name": "Médiathèque de Monaco",
174
+ "title": "Yoga en famille",
175
+ "description": "Partagez un moment unique lors d’une séance de yoga parent/enfant. Venez profiter d’un temps de détente et de bien-être dans une ambiance conviviale. La séance est ouverte à tous, quel que soit votre niveau.",
176
+ "date_start": "2026-03-07",
177
+ "date_end": "2026-03-07",
178
+ "always_current": false,
179
+ "start_time": "10:30",
180
+ "end_time": "11:30",
181
+ "tags": [
182
+ "Jeunesse",
183
+ "Atelier",
184
+ "Bien-être"
185
+ ],
186
+ "price": null,
187
+ "price_value": 0,
188
+ "free": true,
189
+ "url": "https://www.mediatheque.mc/Default/doc/CALENDAR/1647/yoga-en-famille",
190
+ "image_url": "https://www.mediatheque.mc/basicimagedownload.ashx?itemGuid=9CD53647-B3EA-4ED2-A768-FEE790FAF3B6",
191
+ "source": "https://www.mediatheque.mc/search.aspx?SC=CALENDAR_PLANNER_1",
192
+ "scraped_at": "2026-02-28T14:56:46Z"
193
+ },
194
+ {
195
+ "id": "mediatheque_monaco_apres-midi-manga-jeux-et-manga-blabla_2026-03-07",
196
+ "venue_id": "mediatheque_monaco",
197
+ "venue_name": "Médiathèque de Monaco",
198
+ "title": "Après-midi Manga - Jeux et Manga Blabla",
199
+ "description": "L’occasion de vous présenter les nouveautés de la Médiathèque mais aussi et surtout d’échanger avec vous sur vos dernières lectures. LE RDV pour parler : Mangas, Manwhas, webtoon et animés ! On commence avec du jeu ou des quizz…",
200
+ "date_start": "2026-03-07",
201
+ "date_end": "2026-03-07",
202
+ "always_current": false,
203
+ "start_time": "14:30",
204
+ "end_time": "16:30",
205
+ "tags": [
206
+ "Manga",
207
+ "Jeux",
208
+ "Atelier"
209
+ ],
210
+ "price": null,
211
+ "price_value": 0,
212
+ "free": true,
213
+ "url": "https://www.mediatheque.mc/Default/doc/CALENDAR/1648/apres-midi-manga-jeux-et-manga-blabla",
214
+ "image_url": "https://www.mediatheque.mc/basicimagedownload.ashx?itemGuid=6F1FAA66-E2AA-42EF-99A0-5068E6AE10E1",
215
+ "source": "https://www.mediatheque.mc/search.aspx?SC=CALENDAR_PLANNER_1",
216
+ "scraped_at": "2026-02-28T14:56:46Z"
217
+ },
218
+ {
219
+ "id": "mediatheque_monaco_picnic-music-un-concert-sur-grand-ecran-pour-accompagner-votre-pause-dejeuner_2026-03-10",
220
+ "venue_id": "mediatheque_monaco",
221
+ "venue_name": "Médiathèque de Monaco",
222
+ "title": "Picnic Music - Un concert sur grand écran pour accompagner votre pause déjeuner",
223
+ "description": "Envie d’une pause déjeuner musicale ? Venez profiter de ce moment privilégié pour savourer votre repas, seul, entre collègues ou entre amis tout en regardant un concert projeté sur l’écran de l’Atrium de la Médiathèque Caroline. Une programmation variée.",
224
+ "date_start": "2026-03-10",
225
+ "date_end": "2026-03-10",
226
+ "always_current": false,
227
+ "start_time": "12:15",
228
+ "end_time": "13:45",
229
+ "tags": [
230
+ "Musique",
231
+ "Concert",
232
+ "Projection"
233
+ ],
234
+ "price": null,
235
+ "price_value": 0,
236
+ "free": true,
237
+ "url": "https://www.mediatheque.mc/Default/doc/CALENDAR/1687/picnic-music-un-concert-sur-grand-ecran-pour-accompagner-votre-pause-dejeuner",
238
+ "image_url": "https://www.mediatheque.mc/basicimagedownload.ashx?itemGuid=D5D6384D-875B-4230-ABD7-CB0EA4D46F03",
239
+ "source": "https://www.mediatheque.mc/search.aspx?SC=CALENDAR_PLANNER_1",
240
+ "scraped_at": "2026-02-28T14:56:46Z"
241
+ },
242
+ {
243
+ "id": "mediatheque_monaco_cine-club-cycle-les-amerindiens-aujourd-hui-les-chansons-que-mes-freres-m-ont-apprises-de-chloe-zhao_2026-03-10",
244
+ "venue_id": "mediatheque_monaco",
245
+ "venue_name": "Médiathèque de Monaco",
246
+ "title": "Ciné-club - Cycle \"Les Amérindiens aujourd'hui\" : Les Chansons que mes frères m’ont apprises de Chloé Zhao (2015) présenté par Mireille Vercellino",
247
+ "description": "Premier long métrage de Chloé Zhao, Les Chansons que mes frères m’ont apprises est une œuvre sensible et profondément humaine, tournée au cœur de la réserve de Pine Ridge. Porté par une mise en scène épurée et des acteurs non professionnels, le film explore les thèmes de l'identité et de la communauté.",
248
+ "date_start": "2026-03-10",
249
+ "date_end": "2026-03-10",
250
+ "always_current": false,
251
+ "start_time": "18:30",
252
+ "end_time": "20:30",
253
+ "tags": [
254
+ "Cinéma",
255
+ "Projection",
256
+ "Conférence"
257
+ ],
258
+ "price": null,
259
+ "price_value": 0,
260
+ "free": true,
261
+ "url": "https://www.mediatheque.mc/Default/doc/CALENDAR/1649/cine-club-cycle-les-amerindiens-aujourd-hui-les-chansons-que-mes-freres-m-ont-apprises-de-chloe-zhao",
262
+ "image_url": "https://www.mediatheque.mc/basicimagedownload.ashx?itemGuid=CDA73D1A-A784-4F25-9BB7-4BC9A3D16D73",
263
+ "source": "https://www.mediatheque.mc/search.aspx?SC=CALENDAR_PLANNER_1",
264
+ "scraped_at": "2026-02-28T14:56:46Z"
265
+ },
266
+ {
267
+ "id": "mediatheque_monaco_atelier-d-eveil-sensoriel_2026-03-11",
268
+ "venue_id": "mediatheque_monaco",
269
+ "venue_name": "Médiathèque de Monaco",
270
+ "title": "Atelier d’éveil sensoriel",
271
+ "description": "Un monde merveilleux de sons, de couleurs et de textures pour découvrir et explorer. À disposition une multitude d'objets à manipuler pour inviter les tout-petits à éveiller leurs sens en toute douceur.",
272
+ "date_start": "2026-03-11",
273
+ "date_end": "2026-03-11",
274
+ "always_current": false,
275
+ "start_time": "10:30",
276
+ "end_time": "11:15",
277
+ "tags": [
278
+ "Tout-petits",
279
+ "Atelier"
280
+ ],
281
+ "price": null,
282
+ "price_value": 0,
283
+ "free": true,
284
+ "url": "https://www.mediatheque.mc/Default/doc/CALENDAR/1682/atelier-d-eveil-sensoriel",
285
+ "image_url": "https://www.mediatheque.mc/basicimagedownload.ashx?itemGuid=3DBFAB75-307E-4BCC-B2AD-5FB434C1D950",
286
+ "source": "https://www.mediatheque.mc/search.aspx?SC=CALENDAR_PLANNER_1",
287
+ "scraped_at": "2026-02-28T14:56:46Z"
288
+ }
289
+ ]
culture_data/events_musee_oceano.json ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "id": "musee_oceano_animations_gratuites_mon_week_end_aux_musees_2026-01-31",
4
+ "venue_id": "musee_oceano",
5
+ "venue_name": "Musée Océanographique de Monaco",
6
+ "title": "À la découverte des animaux du bord de mer",
7
+ "description": "Rencontre sensorielle avec les espèces du littoral méditerranéen — crabes, étoiles de mer, concombres de mer… Une animation ludique pour apprendre comment ces animaux se déplacent, se nourrissent et survivent dans leur milieu.",
8
+ "date_start": "2026-01-31",
9
+ "date_end": "2026-02-01",
10
+ "always_current": false,
11
+ "start_time": null,
12
+ "end_time": null,
13
+ "tags": [
14
+ "atelier",
15
+ "famille"
16
+ ],
17
+ "price": "Gratuit (avec billet d'entrée au musée)",
18
+ "price_value": 0,
19
+ "free": true,
20
+ "url": null,
21
+ "image_url": null,
22
+ "source": "https://www.oceano.mc/fr/agenda",
23
+ "scraped_at": "2026-02-28T15:34:13Z"
24
+ },
25
+ {
26
+ "id": "musee_oceano_immerseave_vr_mon_week_end_aux_musees_2026-01-31",
27
+ "venue_id": "musee_oceano",
28
+ "venue_name": "Musée Océanographique de Monaco",
29
+ "title": "ImmerSEAve VR – Immersion en réalité virtuelle",
30
+ "description": "Plongez sans vous mouiller dans une Aire Marine Protégée de Méditerranée équipés de casques de réalité virtuelle dernière génération.",
31
+ "date_start": "2026-01-31",
32
+ "date_end": "2026-02-01",
33
+ "always_current": false,
34
+ "start_time": null,
35
+ "end_time": null,
36
+ "tags": [
37
+ "atelier",
38
+ "réalité virtuelle"
39
+ ],
40
+ "price": "Gratuit (avec billet d'entrée au musée)",
41
+ "price_value": 0,
42
+ "free": true,
43
+ "url": null,
44
+ "image_url": null,
45
+ "source": "https://www.oceano.mc/fr/agenda",
46
+ "scraped_at": "2026-02-28T15:34:13Z"
47
+ },
48
+ {
49
+ "id": "musee_oceano_chasse_aux_enigmes_mon_week_end_aux_musees_2026-01-31",
50
+ "venue_id": "musee_oceano",
51
+ "venue_name": "Musée Océanographique de Monaco",
52
+ "title": "Chasse aux énigmes – Jeu-parcours interactif",
53
+ "description": "Partez en famille pour un parcours ludique et éducatif au cœur du Musée : observez les salles, réfléchissez aux indices et relevez les défis pour devenir de véritables Ambassadeurs de l’Océan !",
54
+ "date_start": "2026-01-31",
55
+ "date_end": "2026-02-01",
56
+ "always_current": false,
57
+ "start_time": null,
58
+ "end_time": null,
59
+ "tags": [
60
+ "jeu",
61
+ "famille"
62
+ ],
63
+ "price": "Gratuit (avec billet d'entrée au musée)",
64
+ "price_value": 0,
65
+ "free": true,
66
+ "url": null,
67
+ "image_url": null,
68
+ "source": "https://www.oceano.mc/fr/agenda",
69
+ "scraped_at": "2026-02-28T15:34:13Z"
70
+ },
71
+ {
72
+ "id": "musee_oceano_mediterranee_2050_2026-02-14",
73
+ "venue_id": "musee_oceano",
74
+ "venue_name": "Musée Océanographique de Monaco",
75
+ "title": "Méditerranée 2050",
76
+ "description": "Exposition immersive interactive et spectaculaire. Un voyage spatio-temporel dans la Grande Bleue à différentes époques avant de vous projeter dans le futur en 2050. Quatre espaces immersifs à explorer : Oceanomania, Océano Monaco, Océano Odyssey, My Oceano Med.",
77
+ "date_start": "2026-02-14",
78
+ "date_end": "2026-03-01",
79
+ "always_current": false,
80
+ "start_time": null,
81
+ "end_time": null,
82
+ "tags": [
83
+ "exposition",
84
+ "immersive"
85
+ ],
86
+ "price": "Compris dans le billet d'entrée au musée",
87
+ "price_value": 0,
88
+ "free": true,
89
+ "url": "https://recreanice.fr/exposition-mediterranee-2050-musee-oceanographique-monaco",
90
+ "image_url": "https://recreanice.fr/sites/default/files/2025/imce/oceano_odyssey_cinstitut_oceanographique_monaco_frederic_pacorel_1.jpg",
91
+ "source": "https://www.oceano.mc/fr/agenda",
92
+ "scraped_at": "2026-02-28T15:34:13Z"
93
+ },
94
+ {
95
+ "id": "musee_oceano_un_siecle_dhistoire_en_salle_de_conferences_2026-02-14",
96
+ "venue_id": "musee_oceano",
97
+ "venue_name": "Musée Océanographique de Monaco",
98
+ "title": "Un siècle d’Histoire en Salle de Conférences",
99
+ "description": "Un film de 12 minutes et 8 modules sonores vous invite à découvrir l'histoire des lieux et l’importance des événements qui s’y sont tenus. Venez parcourir 115 ans d’histoire de protection de l’Océan avec les princes de Monaco, le Commandant Cousteau, Jean Malaurie, Anita Conti, Jean-Louis Étienne...",
100
+ "date_start": "2026-02-14",
101
+ "date_end": "2026-03-01",
102
+ "always_current": false,
103
+ "start_time": null,
104
+ "end_time": null,
105
+ "tags": [
106
+ "conférence",
107
+ "histoire"
108
+ ],
109
+ "price": "Compris dans le billet d'entrée au musée",
110
+ "price_value": 0,
111
+ "free": true,
112
+ "url": null,
113
+ "image_url": "https://recreanice.fr/sites/default/files/2025/imce/salle_conferencescinstitut_oceanographique_de_monaco_frederic.jpg",
114
+ "source": "https://www.oceano.mc/fr/agenda",
115
+ "scraped_at": "2026-02-28T15:34:13Z"
116
+ },
117
+ {
118
+ "id": "musee_oceano_animation_immerseave_vr_2026-02-14",
119
+ "venue_id": "musee_oceano",
120
+ "venue_name": "Musée Océanographique de Monaco",
121
+ "title": "Animation ImmerSEAve VR : l'expérience de plongée immersive",
122
+ "description": "Équipés de casques de réalité virtuelle dernière génération, partez comme de véritables plongeurs à la découverte d’une Aire Marine Protégée au cœur de la Méditerranée. Sans masque ni tuba, évoluez librement jusqu'à plus de 45 m de profondeur dans un univers virtuel.",
123
+ "date_start": "2026-02-14",
124
+ "date_end": "2026-03-01",
125
+ "always_current": false,
126
+ "start_time": null,
127
+ "end_time": null,
128
+ "tags": [
129
+ "atelier",
130
+ "réalité virtuelle"
131
+ ],
132
+ "price": "10€ par personne",
133
+ "price_value": 10,
134
+ "free": false,
135
+ "url": "https://billetterie-oceano.tickeasy.com/fr-FR/accueil?utm_source=recreanice&utm_medium=frame&utm_campaign=toussaint",
136
+ "image_url": "https://recreanice.fr/sites/default/files/2025/imce/animation_immerseave_vr_20251030_musee_oceanograpique_monacocinstitut_oceanographique_monaco_frederic_pacorel_167.jpg",
137
+ "source": "https://www.oceano.mc/fr/agenda",
138
+ "scraped_at": "2026-02-28T15:34:13Z"
139
+ },
140
+ {
141
+ "id": "musee_oceano_les_animaux_du_bord_de_mer_2026-02-14",
142
+ "venue_id": "musee_oceano",
143
+ "venue_name": "Musée Océanographique de Monaco",
144
+ "title": "Les Animaux du bord de mer",
145
+ "description": "Vivez en famille une rencontre magique avec les animaux du littoral méditerranéen : étoiles de mer, crabes, concombres de mer... Guidés par les animateurs, vivez une expérience sensorielle inoubliable !",
146
+ "date_start": "2026-02-14",
147
+ "date_end": "2026-03-01",
148
+ "always_current": false,
149
+ "start_time": null,
150
+ "end_time": null,
151
+ "tags": [
152
+ "atelier",
153
+ "famille"
154
+ ],
155
+ "price": "8€ par participant",
156
+ "price_value": 8,
157
+ "free": false,
158
+ "url": "https://billetterie-oceano.tickeasy.com/fr-FR/accueil?utm_source=recreanice&utm_medium=frame&utm_campaign=toussaint",
159
+ "image_url": "https://recreanice.fr/sites/default/files/2025/imce/animation_les_animaux_du_bord_de_mer_20251029_musee_oceanograpique_monacocinstitut_oceanographique_monaco_frederic_pacorel164.jpg",
160
+ "source": "https://www.oceano.mc/fr/agenda",
161
+ "scraped_at": "2026-02-28T15:34:13Z"
162
+ },
163
+ {
164
+ "id": "musee_oceano_escape_game_2026-02-14",
165
+ "venue_id": "musee_oceano",
166
+ "venue_name": "Musée Océanographique de Monaco",
167
+ "title": "Escape Game",
168
+ "description": "Vivez un voyage spatio-temporel à bord du Princesse Alice II, le célèbre bateau laboratoire du Prince Albert Ier ! Votre mission : résoudre des énigmes, décrypter des codes, fouiller les recoins pour rentrer au port et éviter le naufrage.",
169
+ "date_start": "2026-02-14",
170
+ "date_end": "2026-03-01",
171
+ "always_current": false,
172
+ "start_time": null,
173
+ "end_time": null,
174
+ "tags": [
175
+ "escape game",
176
+ "aventure"
177
+ ],
178
+ "price": "À partir de 13€ par personne (Escape Découverte), à partir de 24€ par personne (Escape Expérience)",
179
+ "price_value": 13,
180
+ "free": false,
181
+ "url": "https://billetterie-oceano.tickeasy.com/fr-FR/accueil?utm_source=recreanice&utm_medium=frame&utm_campaign=toussaint",
182
+ "image_url": "https://recreanice.fr/sites/default/files/imce/escape_game_du_musee_oceanographique_de_monaco_-_c_m._dagnino_-_musee_oceanographique.jpg",
183
+ "source": "https://www.oceano.mc/fr/agenda",
184
+ "scraped_at": "2026-02-28T15:34:13Z"
185
+ },
186
+ {
187
+ "id": "musee_oceano_stages_vacances_club_oceano_2026-02-16",
188
+ "venue_id": "musee_oceano",
189
+ "venue_name": "Musée Océanographique de Monaco",
190
+ "title": "Stages vacances pour enfants : Club Océano",
191
+ "description": "Stage de découverte du monde marin pour les 6-8 ans et les 9-12 ans. Thème : Expédition Méditerranée. Une semaine en immersion totale avec accès privilégié aux coulisses du musée, jeux, activités et manipulations autour des océans et des animaux marins.",
192
+ "date_start": "2026-02-16",
193
+ "date_end": "2026-02-20",
194
+ "always_current": false,
195
+ "start_time": "09:00",
196
+ "end_time": "17:00",
197
+ "tags": [
198
+ "stage",
199
+ "enfants",
200
+ "atelier"
201
+ ],
202
+ "price": "450€ les 5 jours avec repas, goûters et collations compris",
203
+ "price_value": 450,
204
+ "free": false,
205
+ "url": "http://www.recreanice.fr/snapper-club-enfants-musee-oceanographique-monaco",
206
+ "image_url": null,
207
+ "source": "https://www.oceano.mc/fr/agenda",
208
+ "scraped_at": "2026-02-28T15:34:13Z"
209
+ },
210
+ {
211
+ "id": "musee_oceano_stages_vacances_club_oceano_2026-02-23",
212
+ "venue_id": "musee_oceano",
213
+ "venue_name": "Musée Océanographique de Monaco",
214
+ "title": "Stages vacances pour enfants : Club Océano",
215
+ "description": "Stage de découverte du monde marin pour les 6-8 ans et les 9-12 ans. Thème : Au cœur des récifs coralliens. Une semaine en immersion totale avec accès privilégié aux coulisses du musée, jeux, activités et manipulations autour des océans et des animaux marins.",
216
+ "date_start": "2026-02-23",
217
+ "date_end": "2026-02-27",
218
+ "always_current": false,
219
+ "start_time": "09:00",
220
+ "end_time": "17:00",
221
+ "tags": [
222
+ "stage",
223
+ "enfants",
224
+ "atelier"
225
+ ],
226
+ "price": "450€ les 5 jours avec repas, goûters et collations compris",
227
+ "price_value": 450,
228
+ "free": false,
229
+ "url": "http://www.recreanice.fr/snapper-club-enfants-musee-oceanographique-monaco",
230
+ "image_url": null,
231
+ "source": "https://www.oceano.mc/fr/agenda",
232
+ "scraped_at": "2026-02-28T15:34:13Z"
233
+ },
234
+ {
235
+ "id": "musee_oceano_vip_visit_musee_oceanographique_de_monaco_2026-03-21",
236
+ "venue_id": "musee_oceano",
237
+ "venue_name": "Musée Océanographique de Monaco",
238
+ "title": "VIP Visit – Musée Océanographique de Monaco",
239
+ "description": "Unique Access for an Explorers Club tour of the iconic Musée Océanographique de Monaco. Founded by Explorers Club Member Albert I, Prince of Monaco, immerse yourself in the interactive storytelling at the world’s preeminent oceanographic museum.",
240
+ "date_start": "2026-03-21",
241
+ "date_end": "2026-03-21",
242
+ "always_current": false,
243
+ "start_time": "12:00",
244
+ "end_time": "15:00",
245
+ "tags": [
246
+ "visite guidée",
247
+ "exposition"
248
+ ],
249
+ "price": null,
250
+ "price_value": 0,
251
+ "free": false,
252
+ "url": null,
253
+ "image_url": null,
254
+ "source": "https://www.oceano.mc/fr/agenda",
255
+ "scraped_at": "2026-02-28T15:34:13Z"
256
+ }
257
+ ]
culture_data/events_theatre_muses.json ADDED
@@ -0,0 +1,591 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "id": "theatre_muses_merci-du-cadeau_2026-03-04",
4
+ "venue_id": "theatre_muses",
5
+ "venue_name": "Théâtre des Muses Monaco",
6
+ "title": "Merci du cadeau !",
7
+ "description": "Spectacle jeune public.",
8
+ "date_start": "2026-03-04",
9
+ "date_end": "2026-03-04",
10
+ "always_current": false,
11
+ "start_time": "15:00",
12
+ "end_time": null,
13
+ "tags": [
14
+ "théâtre",
15
+ "jeune public"
16
+ ],
17
+ "price": "16€",
18
+ "price_value": 16,
19
+ "free": false,
20
+ "url": "https://www.letheatredesmuses.com/programme-enfants/merci-du-cadeau/",
21
+ "image_url": "https://image.jimcdn.com/cdn-cgi/image/width=241%2Cheight=10000%2Cfit=contain%2Cformat=jpg%2C/app/cms/storage/image/path/s87577ae79380f516/image/i45e0465656f2eb7e/version/1769983710/image.jpg",
22
+ "source": "https://www.letheatredesmuses.com/programme-adulte/",
23
+ "scraped_at": "2026-02-28T15:11:48Z"
24
+ },
25
+ {
26
+ "id": "theatre_muses_le-songe-d-une-nuit-d-t_2026-03-05",
27
+ "venue_id": "theatre_muses",
28
+ "venue_name": "Théâtre des Muses Monaco",
29
+ "title": "Le Songe d'une nuit d'été",
30
+ "description": "Pièce de théâtre de William Shakespeare où magie et réalité s’entremêlent.",
31
+ "date_start": "2026-03-05",
32
+ "date_end": "2026-03-05",
33
+ "always_current": false,
34
+ "start_time": "20:00",
35
+ "end_time": null,
36
+ "tags": [
37
+ "théâtre",
38
+ "classique"
39
+ ],
40
+ "price": "31€",
41
+ "price_value": 31,
42
+ "free": false,
43
+ "url": "https://www.letheatredesmuses.com/programme-adulte/songe/",
44
+ "image_url": "https://image.jimcdn.com/cdn-cgi/image/width=244%2Cheight=10000%2Cfit=contain%2Cformat=jpg%2C/app/cms/storage/image/path/s87577ae79380f516/image/i3539267c83999f6c/version/1770588589/image.jpg",
45
+ "source": "https://www.letheatredesmuses.com/programme-adulte/",
46
+ "scraped_at": "2026-02-28T15:11:48Z"
47
+ },
48
+ {
49
+ "id": "theatre_muses_le-songe-d-une-nuit-d-t_2026-03-06",
50
+ "venue_id": "theatre_muses",
51
+ "venue_name": "Théâtre des Muses Monaco",
52
+ "title": "Le Songe d'une nuit d'été",
53
+ "description": "Pièce de théâtre de William Shakespeare où magie et réalité s’entremêlent.",
54
+ "date_start": "2026-03-06",
55
+ "date_end": "2026-03-06",
56
+ "always_current": false,
57
+ "start_time": "20:00",
58
+ "end_time": null,
59
+ "tags": [
60
+ "théâtre",
61
+ "classique"
62
+ ],
63
+ "price": "31€",
64
+ "price_value": 31,
65
+ "free": false,
66
+ "url": "https://www.letheatredesmuses.com/programme-adulte/songe/",
67
+ "image_url": "https://image.jimcdn.com/cdn-cgi/image/width=244%2Cheight=10000%2Cfit=contain%2Cformat=jpg%2C/app/cms/storage/image/path/s87577ae79380f516/image/i3539267c83999f6c/version/1770588589/image.jpg",
68
+ "source": "https://www.letheatredesmuses.com/programme-adulte/",
69
+ "scraped_at": "2026-02-28T15:11:48Z"
70
+ },
71
+ {
72
+ "id": "theatre_muses_merci-du-cadeau_2026-03-07",
73
+ "venue_id": "theatre_muses",
74
+ "venue_name": "Théâtre des Muses Monaco",
75
+ "title": "Merci du cadeau !",
76
+ "description": "Spectacle jeune public.",
77
+ "date_start": "2026-03-07",
78
+ "date_end": "2026-03-07",
79
+ "always_current": false,
80
+ "start_time": "15:00",
81
+ "end_time": null,
82
+ "tags": [
83
+ "théâtre",
84
+ "jeune public"
85
+ ],
86
+ "price": "16€",
87
+ "price_value": 16,
88
+ "free": false,
89
+ "url": "https://www.letheatredesmuses.com/programme-enfants/merci-du-cadeau/",
90
+ "image_url": "https://image.jimcdn.com/cdn-cgi/image/width=241%2Cheight=10000%2Cfit=contain%2Cformat=jpg%2C/app/cms/storage/image/path/s87577ae79380f516/image/i45e0465656f2eb7e/version/1769983710/image.jpg",
91
+ "source": "https://www.letheatredesmuses.com/programme-adulte/",
92
+ "scraped_at": "2026-02-28T15:11:48Z"
93
+ },
94
+ {
95
+ "id": "theatre_muses_le-songe-d-une-nuit-d-t_2026-03-07",
96
+ "venue_id": "theatre_muses",
97
+ "venue_name": "Théâtre des Muses Monaco",
98
+ "title": "Le Songe d'une nuit d'été",
99
+ "description": "Pièce de théâtre de William Shakespeare où magie et réalité s’entremêlent.",
100
+ "date_start": "2026-03-07",
101
+ "date_end": "2026-03-07",
102
+ "always_current": false,
103
+ "start_time": "20:00",
104
+ "end_time": null,
105
+ "tags": [
106
+ "théâtre",
107
+ "classique"
108
+ ],
109
+ "price": "31€",
110
+ "price_value": 31,
111
+ "free": false,
112
+ "url": "https://www.letheatredesmuses.com/programme-adulte/songe/",
113
+ "image_url": "https://image.jimcdn.com/cdn-cgi/image/width=244%2Cheight=10000%2Cfit=contain%2Cformat=jpg%2C/app/cms/storage/image/path/s87577ae79380f516/image/i3539267c83999f6c/version/1770588589/image.jpg",
114
+ "source": "https://www.letheatredesmuses.com/programme-adulte/",
115
+ "scraped_at": "2026-02-28T15:11:48Z"
116
+ },
117
+ {
118
+ "id": "theatre_muses_merci-du-cadeau_2026-03-08",
119
+ "venue_id": "theatre_muses",
120
+ "venue_name": "Théâtre des Muses Monaco",
121
+ "title": "Merci du cadeau !",
122
+ "description": "Spectacle jeune public.",
123
+ "date_start": "2026-03-08",
124
+ "date_end": "2026-03-08",
125
+ "always_current": false,
126
+ "start_time": "15:00",
127
+ "end_time": null,
128
+ "tags": [
129
+ "théâtre",
130
+ "jeune public"
131
+ ],
132
+ "price": "16€",
133
+ "price_value": 16,
134
+ "free": false,
135
+ "url": "https://www.letheatredesmuses.com/programme-enfants/merci-du-cadeau/",
136
+ "image_url": "https://image.jimcdn.com/cdn-cgi/image/width=241%2Cheight=10000%2Cfit=contain%2Cformat=jpg%2C/app/cms/storage/image/path/s87577ae79380f516/image/i45e0465656f2eb7e/version/1769983710/image.jpg",
137
+ "source": "https://www.letheatredesmuses.com/programme-adulte/",
138
+ "scraped_at": "2026-02-28T15:11:48Z"
139
+ },
140
+ {
141
+ "id": "theatre_muses_le-songe-d-une-nuit-d-t_2026-03-08",
142
+ "venue_id": "theatre_muses",
143
+ "venue_name": "Théâtre des Muses Monaco",
144
+ "title": "Le Songe d'une nuit d'été",
145
+ "description": "Pièce de théâtre de William Shakespeare où magie et réalité s’entremêlent.",
146
+ "date_start": "2026-03-08",
147
+ "date_end": "2026-03-08",
148
+ "always_current": false,
149
+ "start_time": "16:30",
150
+ "end_time": null,
151
+ "tags": [
152
+ "théâtre",
153
+ "classique"
154
+ ],
155
+ "price": "31€",
156
+ "price_value": 31,
157
+ "free": false,
158
+ "url": "https://www.letheatredesmuses.com/programme-adulte/songe/",
159
+ "image_url": "https://image.jimcdn.com/cdn-cgi/image/width=244%2Cheight=10000%2Cfit=contain%2Cformat=jpg%2C/app/cms/storage/image/path/s87577ae79380f516/image/i3539267c83999f6c/version/1770588589/image.jpg",
160
+ "source": "https://www.letheatredesmuses.com/programme-adulte/",
161
+ "scraped_at": "2026-02-28T15:11:48Z"
162
+ },
163
+ {
164
+ "id": "theatre_muses_momo-petit-prince-des-bleuets_2026-03-11",
165
+ "venue_id": "theatre_muses",
166
+ "venue_name": "Théâtre des Muses Monaco",
167
+ "title": "Momo, petit prince des bleuets",
168
+ "description": "Spectacle jeune public.",
169
+ "date_start": "2026-03-11",
170
+ "date_end": "2026-03-11",
171
+ "always_current": false,
172
+ "start_time": "15:00",
173
+ "end_time": null,
174
+ "tags": [
175
+ "théâtre",
176
+ "jeune public"
177
+ ],
178
+ "price": "16€",
179
+ "price_value": 16,
180
+ "free": false,
181
+ "url": "https://www.letheatredesmuses.com/programme-enfants/momo/",
182
+ "image_url": "https://image.jimcdn.com/cdn-cgi/image/width=230%2Cheight=10000%2Cfit=contain%2Cformat=jpg%2C/app/cms/storage/image/path/s87577ae79380f516/image/i3f1cc47982610ee4/version/1769983710/image.jpg",
183
+ "source": "https://www.letheatredesmuses.com/programme-adulte/",
184
+ "scraped_at": "2026-02-28T15:11:48Z"
185
+ },
186
+ {
187
+ "id": "theatre_muses_victor-hugo-mon-amour_2026-03-12",
188
+ "venue_id": "theatre_muses",
189
+ "venue_name": "Théâtre des Muses Monaco",
190
+ "title": "Victor Hugo, mon amour",
191
+ "description": "Spectacle mettant en scène l'histoire d'amour entre Victor Hugo et Juliette Drouet.",
192
+ "date_start": "2026-03-12",
193
+ "date_end": "2026-03-12",
194
+ "always_current": false,
195
+ "start_time": "20:00",
196
+ "end_time": null,
197
+ "tags": [
198
+ "théâtre",
199
+ "biographie"
200
+ ],
201
+ "price": "31€",
202
+ "price_value": 31,
203
+ "free": false,
204
+ "url": "https://www.letheatredesmuses.com/programme-adulte/victor-hugo-mon-amour/",
205
+ "image_url": "https://image.jimcdn.com/cdn-cgi/image/width=244%2Cheight=10000%2Cfit=contain%2Cformat=jpg%2C/app/cms/storage/image/path/s87577ae79380f516/image/i3539267c83999f6c/version/1770588589/image.jpg",
206
+ "source": "https://www.letheatredesmuses.com/programme-adulte/",
207
+ "scraped_at": "2026-02-28T15:11:48Z"
208
+ },
209
+ {
210
+ "id": "theatre_muses_victor-hugo-mon-amour_2026-03-13",
211
+ "venue_id": "theatre_muses",
212
+ "venue_name": "Théâtre des Muses Monaco",
213
+ "title": "Victor Hugo, mon amour",
214
+ "description": "Spectacle mettant en scène l'histoire d'amour entre Victor Hugo et Juliette Drouet.",
215
+ "date_start": "2026-03-13",
216
+ "date_end": "2026-03-13",
217
+ "always_current": false,
218
+ "start_time": "20:00",
219
+ "end_time": null,
220
+ "tags": [
221
+ "théâtre",
222
+ "biographie"
223
+ ],
224
+ "price": "31€",
225
+ "price_value": 31,
226
+ "free": false,
227
+ "url": "https://www.letheatredesmuses.com/programme-adulte/victor-hugo-mon-amour/",
228
+ "image_url": "https://image.jimcdn.com/cdn-cgi/image/width=244%2Cheight=10000%2Cfit=contain%2Cformat=jpg%2C/app/cms/storage/image/path/s87577ae79380f516/image/i3539267c83999f6c/version/1770588589/image.jpg",
229
+ "source": "https://www.letheatredesmuses.com/programme-adulte/",
230
+ "scraped_at": "2026-02-28T15:11:48Z"
231
+ },
232
+ {
233
+ "id": "theatre_muses_momo-petit-prince-des-bleuets_2026-03-14",
234
+ "venue_id": "theatre_muses",
235
+ "venue_name": "Théâtre des Muses Monaco",
236
+ "title": "Momo, petit prince des bleuets",
237
+ "description": "Spectacle jeune public.",
238
+ "date_start": "2026-03-14",
239
+ "date_end": "2026-03-14",
240
+ "always_current": false,
241
+ "start_time": "15:00",
242
+ "end_time": null,
243
+ "tags": [
244
+ "théâtre",
245
+ "jeune public"
246
+ ],
247
+ "price": "16€",
248
+ "price_value": 16,
249
+ "free": false,
250
+ "url": "https://www.letheatredesmuses.com/programme-enfants/momo/",
251
+ "image_url": "https://image.jimcdn.com/cdn-cgi/image/width=230%2Cheight=10000%2Cfit=contain%2Cformat=jpg%2C/app/cms/storage/image/path/s87577ae79380f516/image/i3f1cc47982610ee4/version/1769983710/image.jpg",
252
+ "source": "https://www.letheatredesmuses.com/programme-adulte/",
253
+ "scraped_at": "2026-02-28T15:11:48Z"
254
+ },
255
+ {
256
+ "id": "theatre_muses_victor-hugo-mon-amour_2026-03-14",
257
+ "venue_id": "theatre_muses",
258
+ "venue_name": "Théâtre des Muses Monaco",
259
+ "title": "Victor Hugo, mon amour",
260
+ "description": "Spectacle mettant en scène l'histoire d'amour entre Victor Hugo et Juliette Drouet.",
261
+ "date_start": "2026-03-14",
262
+ "date_end": "2026-03-14",
263
+ "always_current": false,
264
+ "start_time": "20:00",
265
+ "end_time": null,
266
+ "tags": [
267
+ "théâtre",
268
+ "biographie"
269
+ ],
270
+ "price": "31€",
271
+ "price_value": 31,
272
+ "free": false,
273
+ "url": "https://www.letheatredesmuses.com/programme-adulte/victor-hugo-mon-amour/",
274
+ "image_url": "https://image.jimcdn.com/cdn-cgi/image/width=244%2Cheight=10000%2Cfit=contain%2Cformat=jpg%2C/app/cms/storage/image/path/s87577ae79380f516/image/i3539267c83999f6c/version/1770588589/image.jpg",
275
+ "source": "https://www.letheatredesmuses.com/programme-adulte/",
276
+ "scraped_at": "2026-02-28T15:11:48Z"
277
+ },
278
+ {
279
+ "id": "theatre_muses_momo-petit-prince-des-bleuets_2026-03-15",
280
+ "venue_id": "theatre_muses",
281
+ "venue_name": "Théâtre des Muses Monaco",
282
+ "title": "Momo, petit prince des bleuets",
283
+ "description": "Spectacle jeune public.",
284
+ "date_start": "2026-03-15",
285
+ "date_end": "2026-03-15",
286
+ "always_current": false,
287
+ "start_time": "15:00",
288
+ "end_time": null,
289
+ "tags": [
290
+ "théâtre",
291
+ "jeune public"
292
+ ],
293
+ "price": "16€",
294
+ "price_value": 16,
295
+ "free": false,
296
+ "url": "https://www.letheatredesmuses.com/programme-enfants/momo/",
297
+ "image_url": "https://image.jimcdn.com/cdn-cgi/image/width=230%2Cheight=10000%2Cfit=contain%2Cformat=jpg%2C/app/cms/storage/image/path/s87577ae79380f516/image/i3f1cc47982610ee4/version/1769983710/image.jpg",
298
+ "source": "https://www.letheatredesmuses.com/programme-adulte/",
299
+ "scraped_at": "2026-02-28T15:11:48Z"
300
+ },
301
+ {
302
+ "id": "theatre_muses_victor-hugo-mon-amour_2026-03-15",
303
+ "venue_id": "theatre_muses",
304
+ "venue_name": "Théâtre des Muses Monaco",
305
+ "title": "Victor Hugo, mon amour",
306
+ "description": "Spectacle mettant en scène l'histoire d'amour entre Victor Hugo et Juliette Drouet.",
307
+ "date_start": "2026-03-15",
308
+ "date_end": "2026-03-15",
309
+ "always_current": false,
310
+ "start_time": "18:00",
311
+ "end_time": null,
312
+ "tags": [
313
+ "théâtre",
314
+ "biographie"
315
+ ],
316
+ "price": "31€",
317
+ "price_value": 31,
318
+ "free": false,
319
+ "url": "https://www.letheatredesmuses.com/programme-adulte/victor-hugo-mon-amour/",
320
+ "image_url": "https://image.jimcdn.com/cdn-cgi/image/width=244%2Cheight=10000%2Cfit=contain%2Cformat=jpg%2C/app/cms/storage/image/path/s87577ae79380f516/image/i3539267c83999f6c/version/1770588589/image.jpg",
321
+ "source": "https://www.letheatredesmuses.com/programme-adulte/",
322
+ "scraped_at": "2026-02-28T15:11:48Z"
323
+ },
324
+ {
325
+ "id": "theatre_muses_la-nuit-du-14-chez-l-onie_2026-03-19",
326
+ "venue_id": "theatre_muses",
327
+ "venue_name": "Théâtre des Muses Monaco",
328
+ "title": "La Nuit du 14, Chez Léonie",
329
+ "description": "Spectacle de théâtre.",
330
+ "date_start": "2026-03-19",
331
+ "date_end": "2026-03-19",
332
+ "always_current": false,
333
+ "start_time": "20:00",
334
+ "end_time": null,
335
+ "tags": [
336
+ "théâtre"
337
+ ],
338
+ "price": "31€",
339
+ "price_value": 31,
340
+ "free": false,
341
+ "url": "https://www.letheatredesmuses.com/programme-adulte/leonie/",
342
+ "image_url": "https://image.jimcdn.com/cdn-cgi/image/width=227%2Cheight=10000%2Cfit=contain%2Cformat=jpg%2C/app/cms/storage/image/path/s87577ae79380f516/image/i7430153f106f9bab/version/1770588589/image.jpg",
343
+ "source": "https://www.letheatredesmuses.com/programme-adulte/",
344
+ "scraped_at": "2026-02-28T15:11:48Z"
345
+ },
346
+ {
347
+ "id": "theatre_muses_la-nuit-du-14-chez-l-onie_2026-03-20",
348
+ "venue_id": "theatre_muses",
349
+ "venue_name": "Théâtre des Muses Monaco",
350
+ "title": "La Nuit du 14, Chez Léonie",
351
+ "description": "Spectacle de théâtre.",
352
+ "date_start": "2026-03-20",
353
+ "date_end": "2026-03-20",
354
+ "always_current": false,
355
+ "start_time": "20:00",
356
+ "end_time": null,
357
+ "tags": [
358
+ "théâtre"
359
+ ],
360
+ "price": "31€",
361
+ "price_value": 31,
362
+ "free": false,
363
+ "url": "https://www.letheatredesmuses.com/programme-adulte/leonie/",
364
+ "image_url": "https://image.jimcdn.com/cdn-cgi/image/width=227%2Cheight=10000%2Cfit=contain%2Cformat=jpg%2C/app/cms/storage/image/path/s87577ae79380f516/image/i7430153f106f9bab/version/1770588589/image.jpg",
365
+ "source": "https://www.letheatredesmuses.com/programme-adulte/",
366
+ "scraped_at": "2026-02-28T15:11:48Z"
367
+ },
368
+ {
369
+ "id": "theatre_muses_la-nuit-du-14-chez-l-onie_2026-03-21",
370
+ "venue_id": "theatre_muses",
371
+ "venue_name": "Théâtre des Muses Monaco",
372
+ "title": "La Nuit du 14, Chez Léonie",
373
+ "description": "Spectacle de théâtre.",
374
+ "date_start": "2026-03-21",
375
+ "date_end": "2026-03-21",
376
+ "always_current": false,
377
+ "start_time": "20:00",
378
+ "end_time": null,
379
+ "tags": [
380
+ "théâtre"
381
+ ],
382
+ "price": "31€",
383
+ "price_value": 31,
384
+ "free": false,
385
+ "url": "https://www.letheatredesmuses.com/programme-adulte/leonie/",
386
+ "image_url": "https://image.jimcdn.com/cdn-cgi/image/width=227%2Cheight=10000%2Cfit=contain%2Cformat=jpg%2C/app/cms/storage/image/path/s87577ae79380f516/image/i7430153f106f9bab/version/1770588589/image.jpg",
387
+ "source": "https://www.letheatredesmuses.com/programme-adulte/",
388
+ "scraped_at": "2026-02-28T15:11:48Z"
389
+ },
390
+ {
391
+ "id": "theatre_muses_la-nuit-du-14-chez-l-onie_2026-03-22",
392
+ "venue_id": "theatre_muses",
393
+ "venue_name": "Théâtre des Muses Monaco",
394
+ "title": "La Nuit du 14, Chez Léonie",
395
+ "description": "Spectacle de théâtre.",
396
+ "date_start": "2026-03-22",
397
+ "date_end": "2026-03-22",
398
+ "always_current": false,
399
+ "start_time": "18:00",
400
+ "end_time": null,
401
+ "tags": [
402
+ "théâtre"
403
+ ],
404
+ "price": "31€",
405
+ "price_value": 31,
406
+ "free": false,
407
+ "url": "https://www.letheatredesmuses.com/programme-adulte/leonie/",
408
+ "image_url": "https://image.jimcdn.com/cdn-cgi/image/width=227%2Cheight=10000%2Cfit=contain%2Cformat=jpg%2C/app/cms/storage/image/path/s87577ae79380f516/image/i7430153f106f9bab/version/1770588589/image.jpg",
409
+ "source": "https://www.letheatredesmuses.com/programme-adulte/",
410
+ "scraped_at": "2026-02-28T15:11:48Z"
411
+ },
412
+ {
413
+ "id": "theatre_muses_l-eau-l_2026-03-25",
414
+ "venue_id": "theatre_muses",
415
+ "venue_name": "Théâtre des Muses Monaco",
416
+ "title": "L'Eau-Là",
417
+ "description": "Spectacle jeune public.",
418
+ "date_start": "2026-03-25",
419
+ "date_end": "2026-03-25",
420
+ "always_current": false,
421
+ "start_time": "15:00",
422
+ "end_time": null,
423
+ "tags": [
424
+ "théâtre",
425
+ "jeune public"
426
+ ],
427
+ "price": "16€",
428
+ "price_value": 16,
429
+ "free": false,
430
+ "url": "https://www.letheatredesmuses.com/programme-enfants/leau-la/",
431
+ "image_url": "https://image.jimcdn.com/cdn-cgi/image/width=244%2Cheight=10000%2Cfit=contain%2Cformat=jpg%2C/app/cms/storage/image/path/s87577ae79380f516/image/i3607298c822ce43f/version/1769983710/image.jpg",
432
+ "source": "https://www.letheatredesmuses.com/programme-adulte/",
433
+ "scraped_at": "2026-02-28T15:11:48Z"
434
+ },
435
+ {
436
+ "id": "theatre_muses_l-homme-et-le-p-cheur_2026-03-25",
437
+ "venue_id": "theatre_muses",
438
+ "venue_name": "Théâtre des Muses Monaco",
439
+ "title": "L'Homme et le Pêcheur",
440
+ "description": "Spectacle de théâtre.",
441
+ "date_start": "2026-03-25",
442
+ "date_end": "2026-03-25",
443
+ "always_current": false,
444
+ "start_time": "19:30",
445
+ "end_time": null,
446
+ "tags": [
447
+ "théâtre"
448
+ ],
449
+ "price": "31€",
450
+ "price_value": 31,
451
+ "free": false,
452
+ "url": "https://www.letheatredesmuses.com/programme-adulte/l-homme-et-le-pecheur/",
453
+ "image_url": "https://image.jimcdn.com/cdn-cgi/image/width=244%2Cheight=10000%2Cfit=contain%2Cformat=jpg%2C/app/cms/storage/image/path/s87577ae79380f516/image/id56cbe03ede41a32/version/1770588589/image.jpg",
454
+ "source": "https://www.letheatredesmuses.com/programme-adulte/",
455
+ "scraped_at": "2026-02-28T15:11:48Z"
456
+ },
457
+ {
458
+ "id": "theatre_muses_l-homme-et-le-p-cheur_2026-03-26",
459
+ "venue_id": "theatre_muses",
460
+ "venue_name": "Théâtre des Muses Monaco",
461
+ "title": "L'Homme et le Pêcheur",
462
+ "description": "Spectacle de théâtre.",
463
+ "date_start": "2026-03-26",
464
+ "date_end": "2026-03-26",
465
+ "always_current": false,
466
+ "start_time": "19:30",
467
+ "end_time": null,
468
+ "tags": [
469
+ "théâtre"
470
+ ],
471
+ "price": "31€",
472
+ "price_value": 31,
473
+ "free": false,
474
+ "url": "https://www.letheatredesmuses.com/programme-adulte/l-homme-et-le-pecheur/",
475
+ "image_url": "https://image.jimcdn.com/cdn-cgi/image/width=244%2Cheight=10000%2Cfit=contain%2Cformat=jpg%2C/app/cms/storage/image/path/s87577ae79380f516/image/id56cbe03ede41a32/version/1770588589/image.jpg",
476
+ "source": "https://www.letheatredesmuses.com/programme-adulte/",
477
+ "scraped_at": "2026-02-28T15:11:48Z"
478
+ },
479
+ {
480
+ "id": "theatre_muses_l-homme-et-le-p-cheur_2026-03-27",
481
+ "venue_id": "theatre_muses",
482
+ "venue_name": "Théâtre des Muses Monaco",
483
+ "title": "L'Homme et le Pêcheur",
484
+ "description": "Spectacle de théâtre.",
485
+ "date_start": "2026-03-27",
486
+ "date_end": "2026-03-27",
487
+ "always_current": false,
488
+ "start_time": "19:30",
489
+ "end_time": null,
490
+ "tags": [
491
+ "théâtre"
492
+ ],
493
+ "price": "31€",
494
+ "price_value": 31,
495
+ "free": false,
496
+ "url": "https://www.letheatredesmuses.com/programme-adulte/l-homme-et-le-pecheur/",
497
+ "image_url": "https://image.jimcdn.com/cdn-cgi/image/width=244%2Cheight=10000%2Cfit=contain%2Cformat=jpg%2C/app/cms/storage/image/path/s87577ae79380f516/image/id56cbe03ede41a32/version/1770588589/image.jpg",
498
+ "source": "https://www.letheatredesmuses.com/programme-adulte/",
499
+ "scraped_at": "2026-02-28T15:11:48Z"
500
+ },
501
+ {
502
+ "id": "theatre_muses_l-eau-l_2026-03-28",
503
+ "venue_id": "theatre_muses",
504
+ "venue_name": "Théâtre des Muses Monaco",
505
+ "title": "L'Eau-Là",
506
+ "description": "Spectacle jeune public.",
507
+ "date_start": "2026-03-28",
508
+ "date_end": "2026-03-28",
509
+ "always_current": false,
510
+ "start_time": "15:00",
511
+ "end_time": null,
512
+ "tags": [
513
+ "théâtre",
514
+ "jeune public"
515
+ ],
516
+ "price": "16€",
517
+ "price_value": 16,
518
+ "free": false,
519
+ "url": "https://www.letheatredesmuses.com/programme-enfants/leau-la/",
520
+ "image_url": "https://image.jimcdn.com/cdn-cgi/image/width=244%2Cheight=10000%2Cfit=contain%2Cformat=jpg%2C/app/cms/storage/image/path/s87577ae79380f516/image/i3607298c822ce43f/version/1769983710/image.jpg",
521
+ "source": "https://www.letheatredesmuses.com/programme-adulte/",
522
+ "scraped_at": "2026-02-28T15:11:48Z"
523
+ },
524
+ {
525
+ "id": "theatre_muses_l-homme-et-le-p-cheur_2026-03-28",
526
+ "venue_id": "theatre_muses",
527
+ "venue_name": "Théâtre des Muses Monaco",
528
+ "title": "L'Homme et le Pêcheur",
529
+ "description": "Spectacle de théâtre.",
530
+ "date_start": "2026-03-28",
531
+ "date_end": "2026-03-28",
532
+ "always_current": false,
533
+ "start_time": "19:30",
534
+ "end_time": null,
535
+ "tags": [
536
+ "théâtre"
537
+ ],
538
+ "price": "31€",
539
+ "price_value": 31,
540
+ "free": false,
541
+ "url": "https://www.letheatredesmuses.com/programme-adulte/l-homme-et-le-pecheur/",
542
+ "image_url": "https://image.jimcdn.com/cdn-cgi/image/width=244%2Cheight=10000%2Cfit=contain%2Cformat=jpg%2C/app/cms/storage/image/path/s87577ae79380f516/image/id56cbe03ede41a32/version/1770588589/image.jpg",
543
+ "source": "https://www.letheatredesmuses.com/programme-adulte/",
544
+ "scraped_at": "2026-02-28T15:11:48Z"
545
+ },
546
+ {
547
+ "id": "theatre_muses_l-eau-l_2026-03-29",
548
+ "venue_id": "theatre_muses",
549
+ "venue_name": "Théâtre des Muses Monaco",
550
+ "title": "L'Eau-Là",
551
+ "description": "Spectacle jeune public.",
552
+ "date_start": "2026-03-29",
553
+ "date_end": "2026-03-29",
554
+ "always_current": false,
555
+ "start_time": "16:00",
556
+ "end_time": null,
557
+ "tags": [
558
+ "théâtre",
559
+ "jeune public"
560
+ ],
561
+ "price": "16€",
562
+ "price_value": 16,
563
+ "free": false,
564
+ "url": "https://www.letheatredesmuses.com/programme-enfants/leau-la/",
565
+ "image_url": "https://image.jimcdn.com/cdn-cgi/image/width=244%2Cheight=10000%2Cfit=contain%2Cformat=jpg%2C/app/cms/storage/image/path/s87577ae79380f516/image/i3607298c822ce43f/version/1769983710/image.jpg",
566
+ "source": "https://www.letheatredesmuses.com/programme-adulte/",
567
+ "scraped_at": "2026-02-28T15:11:48Z"
568
+ },
569
+ {
570
+ "id": "theatre_muses_l-homme-et-le-p-cheur_2026-03-29",
571
+ "venue_id": "theatre_muses",
572
+ "venue_name": "Théâtre des Muses Monaco",
573
+ "title": "L'Homme et le Pêcheur",
574
+ "description": "Spectacle de théâtre.",
575
+ "date_start": "2026-03-29",
576
+ "date_end": "2026-03-29",
577
+ "always_current": false,
578
+ "start_time": "18:00",
579
+ "end_time": null,
580
+ "tags": [
581
+ "théâtre"
582
+ ],
583
+ "price": "31€",
584
+ "price_value": 31,
585
+ "free": false,
586
+ "url": "https://www.letheatredesmuses.com/programme-adulte/l-homme-et-le-pecheur/",
587
+ "image_url": "https://image.jimcdn.com/cdn-cgi/image/width=244%2Cheight=10000%2Cfit=contain%2Cformat=jpg%2C/app/cms/storage/image/path/s87577ae79380f516/image/id56cbe03ede41a32/version/1770588589/image.jpg",
588
+ "source": "https://www.letheatredesmuses.com/programme-adulte/",
589
+ "scraped_at": "2026-02-28T15:11:48Z"
590
+ }
591
+ ]
culture_data/events_theatre_princesse_grace.json ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "id": "theatre_princesse_grace_du-charbon-dans-les-veines_2026-03-03",
4
+ "venue_id": "theatre_princesse_grace",
5
+ "venue_name": "Théâtre Princesse Grace Monaco",
6
+ "title": "DU CHARBON DANS LES VEINES",
7
+ "description": "Jean-Philippe Daguerre",
8
+ "date_start": "2026-03-03",
9
+ "date_end": null,
10
+ "always_current": false,
11
+ "start_time": "20:00",
12
+ "end_time": null,
13
+ "tags": [
14
+ "Théâtre"
15
+ ],
16
+ "price": null,
17
+ "price_value": 0,
18
+ "free": false,
19
+ "url": "https://www.tpgmonaco.mc/fr/2025-2026/spectacle/322/du-charbon-dans-les-veines",
20
+ "image_url": "https://www.tpgmonaco.mc/medias_upload/moxie/26_Spectacles/Du-charbon-/DUCHARBONDANSLESVEINES.jpg",
21
+ "source": "https://www.tpgmonaco.mc/fr/2025-2026/programme",
22
+ "scraped_at": "2026-03-01T12:08:23Z"
23
+ },
24
+ {
25
+ "id": "theatre_princesse_grace_un-succ-s-fou_2026-03-10",
26
+ "venue_id": "theatre_princesse_grace",
27
+ "venue_name": "Théâtre Princesse Grace Monaco",
28
+ "title": "UN SUCCÈS FOU",
29
+ "description": "texte Sébastien Castro",
30
+ "date_start": "2026-03-10",
31
+ "date_end": null,
32
+ "always_current": false,
33
+ "start_time": "20:00",
34
+ "end_time": null,
35
+ "tags": [
36
+ "Théâtre"
37
+ ],
38
+ "price": null,
39
+ "price_value": 0,
40
+ "free": false,
41
+ "url": "https://www.tpgmonaco.mc/fr/2025-2026/spectacle/323/un-succès-fou",
42
+ "image_url": "https://www.tpgmonaco.mc/medias_upload/moxie/26_Spectacles/Un-succes/UNSUCCESFOU.jpg",
43
+ "source": "https://www.tpgmonaco.mc/fr/2025-2026/programme",
44
+ "scraped_at": "2026-03-01T12:08:23Z"
45
+ },
46
+ {
47
+ "id": "theatre_princesse_grace_corps-sportif-corps-esth-tique_2026-03-12",
48
+ "venue_id": "theatre_princesse_grace",
49
+ "venue_name": "Théâtre Princesse Grace Monaco",
50
+ "title": "Corps sportif, corps esthétique",
51
+ "description": null,
52
+ "date_start": "2026-03-12",
53
+ "date_end": null,
54
+ "always_current": false,
55
+ "start_time": "19:00",
56
+ "end_time": null,
57
+ "tags": [
58
+ "Philo"
59
+ ],
60
+ "price": null,
61
+ "price_value": 0,
62
+ "free": false,
63
+ "url": "https://www.tpgmonaco.mc/fr/2025-2026/spectacle/335/corps-sportif-corps-esthétique",
64
+ "image_url": "https://www.tpgmonaco.mc/medias_upload/moxie/Rencontres-Philo/34124.jpg",
65
+ "source": "https://www.tpgmonaco.mc/fr/2025-2026/programme",
66
+ "scraped_at": "2026-03-01T12:08:23Z"
67
+ },
68
+ {
69
+ "id": "theatre_princesse_grace_entre-les-deux_2026-03-20",
70
+ "venue_id": "theatre_princesse_grace",
71
+ "venue_name": "Théâtre Princesse Grace Monaco",
72
+ "title": "ENTRE LES DEUX",
73
+ "description": "Un spectacle de stand-up de Panayotis Pascot",
74
+ "date_start": "2026-03-20",
75
+ "date_end": null,
76
+ "always_current": false,
77
+ "start_time": "20:00",
78
+ "end_time": null,
79
+ "tags": [
80
+ "Théâtre"
81
+ ],
82
+ "price": null,
83
+ "price_value": 0,
84
+ "free": false,
85
+ "url": "https://www.tpgmonaco.mc/fr/2025-2026/spectacle/324/entre-les-deux",
86
+ "image_url": "https://www.tpgmonaco.mc/medias_upload/moxie/26_Spectacles/Entre-les-deux/ENTRELESDEUX.jpg",
87
+ "source": "https://www.tpgmonaco.mc/fr/2025-2026/programme",
88
+ "scraped_at": "2026-03-01T12:08:23Z"
89
+ },
90
+ {
91
+ "id": "theatre_princesse_grace_c-est-pas-facile-d-tre-heureux-quand-on-va-mal_2026-03-26",
92
+ "venue_id": "theatre_princesse_grace",
93
+ "venue_name": "Théâtre Princesse Grace Monaco",
94
+ "title": "C’EST PAS FACILE D’ÊTRE HEUREUX QUAND ON VA MAL",
95
+ "description": "texte Rudy Milstein",
96
+ "date_start": "2026-03-26",
97
+ "date_end": null,
98
+ "always_current": false,
99
+ "start_time": "20:00",
100
+ "end_time": null,
101
+ "tags": [
102
+ "Théâtre"
103
+ ],
104
+ "price": null,
105
+ "price_value": 0,
106
+ "free": false,
107
+ "url": "https://www.tpgmonaco.mc/fr/2025-2026/spectacle/325/c-est-pas-facile-d-être-heureux-quand-on-va-mal",
108
+ "image_url": "https://www.tpgmonaco.mc/medias_upload/moxie/26_Spectacles/Cest-pas-facile-/CESTPASFACILEDETREHEUREUX.jpg",
109
+ "source": "https://www.tpgmonaco.mc/fr/2025-2026/programme",
110
+ "scraped_at": "2026-03-01T12:08:23Z"
111
+ },
112
+ {
113
+ "id": "theatre_princesse_grace_la-tour-de-constance_2026-03-31",
114
+ "venue_id": "theatre_princesse_grace",
115
+ "venue_name": "Théâtre Princesse Grace Monaco",
116
+ "title": "LA TOUR DE CONSTANCE",
117
+ "description": "texte Guillaume Vincent",
118
+ "date_start": "2026-03-31",
119
+ "date_end": null,
120
+ "always_current": false,
121
+ "start_time": "20:00",
122
+ "end_time": null,
123
+ "tags": [
124
+ "Théâtre"
125
+ ],
126
+ "price": null,
127
+ "price_value": 0,
128
+ "free": false,
129
+ "url": "https://www.tpgmonaco.mc/fr/2025-2026/spectacle/326/la-tour-de-constance",
130
+ "image_url": "https://www.tpgmonaco.mc/medias_upload/moxie/26_Spectacles/La-tour-de-constance/LATOURDECONSTANCE.jpg",
131
+ "source": "https://www.tpgmonaco.mc/fr/2025-2026/programme",
132
+ "scraped_at": "2026-03-01T12:08:23Z"
133
+ },
134
+ {
135
+ "id": "theatre_princesse_grace_la-gratitude_2026-04-02",
136
+ "venue_id": "theatre_princesse_grace",
137
+ "venue_name": "Théâtre Princesse Grace Monaco",
138
+ "title": "La gratitude",
139
+ "description": null,
140
+ "date_start": "2026-04-02",
141
+ "date_end": null,
142
+ "always_current": false,
143
+ "start_time": "19:00",
144
+ "end_time": null,
145
+ "tags": [
146
+ "Philo"
147
+ ],
148
+ "price": null,
149
+ "price_value": 0,
150
+ "free": false,
151
+ "url": "https://www.tpgmonaco.mc/fr/2025-2026/spectacle/336/la-gratitude",
152
+ "image_url": "https://www.tpgmonaco.mc/medias_upload/moxie/Rencontres-Philo/34124.jpg",
153
+ "source": "https://www.tpgmonaco.mc/fr/2025-2026/programme",
154
+ "scraped_at": "2026-03-01T12:08:23Z"
155
+ },
156
+ {
157
+ "id": "theatre_princesse_grace_la-v-rit_2026-04-09",
158
+ "venue_id": "theatre_princesse_grace",
159
+ "venue_name": "Théâtre Princesse Grace Monaco",
160
+ "title": "LA VÉRITÉ",
161
+ "description": "texte Florian Zeller",
162
+ "date_start": "2026-04-09",
163
+ "date_end": null,
164
+ "always_current": false,
165
+ "start_time": "20:00",
166
+ "end_time": null,
167
+ "tags": [
168
+ "Théâtre"
169
+ ],
170
+ "price": null,
171
+ "price_value": 0,
172
+ "free": false,
173
+ "url": "https://www.tpgmonaco.mc/fr/2025-2026/spectacle/327/la-vérité",
174
+ "image_url": "https://www.tpgmonaco.mc/medias_upload/moxie/26_Spectacles/La-verite/LAVERITE.jpg",
175
+ "source": "https://www.tpgmonaco.mc/fr/2025-2026/programme",
176
+ "scraped_at": "2026-03-01T12:08:23Z"
177
+ },
178
+ {
179
+ "id": "theatre_princesse_grace_le-prix_2026-04-23",
180
+ "venue_id": "theatre_princesse_grace",
181
+ "venue_name": "Théâtre Princesse Grace Monaco",
182
+ "title": "LE PRIX",
183
+ "description": "texte Cyril Gely",
184
+ "date_start": "2026-04-23",
185
+ "date_end": null,
186
+ "always_current": false,
187
+ "start_time": "20:00",
188
+ "end_time": null,
189
+ "tags": [
190
+ "Théâtre"
191
+ ],
192
+ "price": null,
193
+ "price_value": 0,
194
+ "free": false,
195
+ "url": "https://www.tpgmonaco.mc/fr/2025-2026/spectacle/328/le-prix",
196
+ "image_url": "https://www.tpgmonaco.mc/medias_upload/moxie/26_Spectacles/Le-prix/prix.jpg",
197
+ "source": "https://www.tpgmonaco.mc/fr/2025-2026/programme",
198
+ "scraped_at": "2026-03-01T12:08:23Z"
199
+ },
200
+ {
201
+ "id": "theatre_princesse_grace_la-veuve-rus-e_2026-04-29",
202
+ "venue_id": "theatre_princesse_grace",
203
+ "venue_name": "Théâtre Princesse Grace Monaco",
204
+ "title": "LA VEUVE RUSÉE",
205
+ "description": "texte Carlo Goldoni",
206
+ "date_start": "2026-04-29",
207
+ "date_end": null,
208
+ "always_current": false,
209
+ "start_time": "20:00",
210
+ "end_time": null,
211
+ "tags": [
212
+ "Théâtre"
213
+ ],
214
+ "price": null,
215
+ "price_value": 0,
216
+ "free": false,
217
+ "url": "https://www.tpgmonaco.mc/fr/2025-2026/spectacle/329/la-veuve-rusée",
218
+ "image_url": "https://www.tpgmonaco.mc/medias_upload/moxie/26_Spectacles/La-veuve-ruse/LAVEUVERUSEE.jpg",
219
+ "source": "https://www.tpgmonaco.mc/fr/2025-2026/programme",
220
+ "scraped_at": "2026-03-01T12:08:23Z"
221
+ },
222
+ {
223
+ "id": "theatre_princesse_grace_mon-jour-de-chance_2026-05-13",
224
+ "venue_id": "theatre_princesse_grace",
225
+ "venue_name": "Théâtre Princesse Grace Monaco",
226
+ "title": "MON JOUR DE CHANCE",
227
+ "description": "texte Patrick Haudecoeur et Gérald Sibleyras",
228
+ "date_start": "2026-05-13",
229
+ "date_end": null,
230
+ "always_current": false,
231
+ "start_time": "20:00",
232
+ "end_time": null,
233
+ "tags": [
234
+ "Théâtre"
235
+ ],
236
+ "price": null,
237
+ "price_value": 0,
238
+ "free": false,
239
+ "url": "https://www.tpgmonaco.mc/fr/2025-2026/spectacle/330/mon-jour-de-chance",
240
+ "image_url": "https://www.tpgmonaco.mc/medias_upload/moxie/26_Spectacles/Mon-jour-de-chance/MONJOURDECHANCE.jpg",
241
+ "source": "https://www.tpgmonaco.mc/fr/2025-2026/programme",
242
+ "scraped_at": "2026-03-01T12:08:23Z"
243
+ }
244
+ ]
main.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import shutil
3
+ import tempfile
4
+ from pathlib import Path
5
+
6
+ from fastapi import FastAPI, File, UploadFile
7
+ from fastapi.responses import FileResponse, Response
8
+ from fastapi.staticfiles import StaticFiles
9
+ from pydantic import BaseModel
10
+ from typing import Optional
11
+
12
+ from agent import respond
13
+ from agent.elevenlabs_tts import list_voices, speak
14
+ from agent.voxtral import transcribe
15
+ from agent import get_last_scraped_at
16
+
17
+ app = FastAPI()
18
+
19
+
20
+ class ChatRequest(BaseModel):
21
+ message: str
22
+ history: list
23
+ provider: str = "mistral"
24
+ model: Optional[str] = None
25
+
26
+
27
+ class TTSRequest(BaseModel):
28
+ text: str
29
+ voice_id: Optional[str] = None
30
+
31
+
32
+ @app.get("/")
33
+ async def root():
34
+ return FileResponse("static/index.html")
35
+
36
+
37
+ @app.post("/chat")
38
+ async def chat(body: ChatRequest):
39
+ try:
40
+ out = respond(
41
+ body.message,
42
+ body.history,
43
+ provider=body.provider,
44
+ model=body.model,
45
+ )
46
+ if isinstance(out, dict):
47
+ return out
48
+ return {"response": out or ""}
49
+ except Exception as e:
50
+ return {"response": f"Désolé, une erreur s'est produite : {e}"}
51
+
52
+
53
+ @app.post("/transcribe")
54
+ async def transcribe_audio(file: UploadFile = File(...)):
55
+ path = None
56
+ try:
57
+ suffix = Path(file.filename or "").suffix or ".webm"
58
+ with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
59
+ shutil.copyfileobj(file.file, tmp)
60
+ path = tmp.name
61
+ text = (transcribe(path) or "").strip()
62
+ return {"text": text}
63
+ except Exception:
64
+ return {"text": ""}
65
+ finally:
66
+ if path and os.path.exists(path):
67
+ try:
68
+ os.unlink(path)
69
+ except OSError:
70
+ pass
71
+
72
+
73
+ @app.post("/tts")
74
+ async def text_to_speech(body: TTSRequest):
75
+ if not (body.text or "").strip():
76
+ return Response(status_code=400, content="Empty text")
77
+ raw = speak(body.text.strip(), voice_id=body.voice_id or None)
78
+ if not raw:
79
+ return Response(status_code=503, content="TTS unavailable")
80
+ return Response(content=raw, media_type="audio/mpeg")
81
+
82
+
83
+ @app.get("/voices")
84
+ async def voices():
85
+ try:
86
+ return list_voices()
87
+ except Exception:
88
+ return []
89
+
90
+
91
+ @app.get("/last-scraped")
92
+ async def last_scraped():
93
+ return {"last_scraped_at": get_last_scraped_at()}
94
+
95
+
96
+ app.mount("/static", StaticFiles(directory="static"), name="static")
97
+
98
+ if __name__ == "__main__":
99
+ import uvicorn
100
+ uvicorn.run("main:app", host="0.0.0.0", port=7860, reload=True)
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ openai
2
+ python-dotenv
3
+ dateparser
4
+ langdetect
5
+ requests
6
+ numpy
7
+ fastapi
8
+ uvicorn
9
+ python-multipart
static/index.html ADDED
@@ -0,0 +1,1009 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Monaco Cultural Agent</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link href="https://fonts.googleapis.com/css2?family=Rajdhani:wght@400;500;600;700&family=Inter:wght@300;400;500&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
9
+ <style>
10
+ :root {
11
+ --bg-primary: #0a0a0a;
12
+ --bg-secondary: #111111;
13
+ --bg-tertiary: #1a1a1a;
14
+ --accent-red: #e8002d;
15
+ --accent-red-dim:rgba(232,0,45,0.12);
16
+ --accent-gold: #c9a84c;
17
+ --accent-gold-l: #e8c870;
18
+ --text-primary: #f0f0f0;
19
+ --text-secondary:#555555;
20
+ --border: #1f1f1f;
21
+ }
22
+ body.light-mode {
23
+ --bg-primary: #ffffff;
24
+ --bg-secondary: #f4f4f4;
25
+ --bg-tertiary: #eaeaea;
26
+ --text-primary: #111111;
27
+ --text-secondary:#777777;
28
+ --border: #d4d4d4;
29
+ }
30
+ body.light-mode .msg.agent li { color: #555; }
31
+ body.light-mode .s-source { color: #888; }
32
+ #theme-btn { background:transparent; border:1px solid var(--border); border-radius:3px; color:var(--text-secondary); font-size:14px; cursor:pointer; padding:4px 10px; transition:all 120ms; line-height:1; }
33
+ #theme-btn:hover { border-color:var(--accent-gold); color:var(--accent-gold); }
34
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
35
+ body {
36
+ background: var(--bg-primary);
37
+ color: var(--text-primary);
38
+ font-family: 'Inter', sans-serif;
39
+ height: 100vh;
40
+ overflow: hidden;
41
+ position: relative;
42
+ }
43
+ body::before {
44
+ content:'';
45
+ position:fixed;
46
+ inset:0;
47
+ background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.04'/%3E%3C/svg%3E");
48
+ pointer-events:none;
49
+ z-index:0;
50
+ }
51
+ body { z-index:1; }
52
+ #header, #app { position:relative; z-index:1; }
53
+ #header {
54
+ display:flex; align-items:center;
55
+ padding: 0 28px; height: 58px;
56
+ border-bottom: 2px solid var(--accent-red);
57
+ background: var(--bg-primary);
58
+ gap: 16px; flex-shrink: 0;
59
+ }
60
+ .logo { font-family:'Rajdhani',sans-serif; font-size:20px; font-weight:700; letter-spacing:4px; text-transform:uppercase; color:var(--text-primary); }
61
+ .logo em { color:var(--accent-red); font-style:normal; }
62
+ .sep { width:1px; height:18px; background:var(--border); }
63
+ .tagline { font-size:10px; color:var(--text-secondary); letter-spacing:2.5px; text-transform:uppercase; font-weight:300; }
64
+ .header-right { display:flex; align-items:center; gap:12px; margin-left:auto; }
65
+ .badge { font-family:'JetBrains Mono',monospace; font-size:9px; color:var(--accent-gold); border:1px solid rgba(201,168,76,0.35); padding:3px 10px; letter-spacing:2px; text-transform:uppercase; }
66
+ #app { display:flex; height:calc(100vh - 58px); overflow:hidden; }
67
+ #chat-area { flex:1; display:flex; flex-direction:column; border-right:1px solid var(--border); overflow:hidden; }
68
+ #messages { flex:1; overflow-y:auto; padding:28px 32px; display:flex; flex-direction:column; gap:14px; scroll-behavior:smooth; }
69
+ #messages::-webkit-scrollbar { width:3px; }
70
+ #messages::-webkit-scrollbar-thumb { background:var(--accent-red); border-radius:2px; }
71
+ #messages::-webkit-scrollbar-track { background:transparent; }
72
+ .msg-wrap { display:flex; flex-direction:column; gap:4px; animation:fadeUp 0.2s ease-out; }
73
+ @keyframes fadeUp { from{opacity:0;transform:translateY(8px)} to{opacity:1;transform:translateY(0)} }
74
+ .msg-label { font-size:9px; letter-spacing:2px; text-transform:uppercase; font-family:'JetBrains Mono',monospace; padding:0 4px; }
75
+ .msg-label.user-label { color:rgba(232,0,45,0.6); text-align:right; }
76
+ .msg-label.agent-label { color:var(--accent-gold); text-align:left; }
77
+ .msg { padding:11px 16px; border-radius:4px; font-size:14px; line-height:1.65; max-width:78%; }
78
+ .msg.user { align-self:flex-end; background:var(--bg-tertiary); border:1px solid var(--border); border-right:3px solid var(--accent-red); box-shadow:2px 2px 16px rgba(232,0,45,0.06); }
79
+ .msg.agent { align-self:flex-start; background:var(--bg-tertiary); border:1px solid var(--border); border-right:3px solid var(--accent-gold); box-shadow:2px 2px 16px rgba(232, 217, 0, 0.06); max-width:78%; }
80
+ .msg.agent ul { padding-left:18px; margin-top:6px; }
81
+ .msg.agent li { margin-bottom:6px; color:#ccc; }
82
+ .agent-intro { color:var(--text-primary); font-weight:500; margin-bottom:12px; }
83
+ .event-list { list-style:none; padding-left:0; margin:0; }
84
+ .event-item { margin-bottom:14px; padding-left:0; color:var(--text-secondary); font-size:13px; }
85
+ .event-item::before { content:''; display:inline-block; width:6px; height:6px; border-radius:50%; background:var(--text-secondary); margin-right:10px; vertical-align:middle; }
86
+ .event-title { color:var(--accent-gold-l); font-weight:500; display:inline; margin-bottom:0; }
87
+ .event-meta { color:var(--text-secondary); font-size:12px; margin-top:2px; margin-bottom:4px; }
88
+ .agent-body.event-meta { margin-top:8px; }
89
+ .event-link { color:var(--accent-red); text-decoration:none; font-size:12px; display:inline-block; margin-top:2px; }
90
+ .event-link:hover { text-decoration:underline; }
91
+ .msg.agent a { color:var(--accent-red); text-decoration:none; font-size:12px; }
92
+ .msg.agent a:hover { text-decoration:underline; }
93
+ .msg-audio { display:flex; align-items:center; gap:8px; margin-top:10px; padding-top:8px; border-top:1px solid var(--border); }
94
+ .play-btn { width:28px; height:28px; border-radius:50%; background:transparent; border:1px solid var(--accent-gold); color:var(--accent-gold); font-size:10px; cursor:pointer; display:flex; align-items:center; justify-content:center; transition:all 120ms; flex-shrink:0; }
95
+ .play-btn:hover { background:rgba(201,168,76,0.12); }
96
+ .play-btn.loading { border-color:transparent; border-top-color:var(--accent-gold); animation:spin 0.7s linear infinite; cursor:default; }
97
+ @keyframes spin { to { transform:rotate(360deg); } }
98
+ .audio-bar { flex:1; height:2px; background:var(--border); border-radius:1px; cursor:pointer; position:relative; }
99
+ .audio-progress { height:100%; background:var(--accent-gold); border-radius:1px; width:0%; transition:width 0.1s; }
100
+ .audio-time { font-family:'JetBrains Mono',monospace; font-size:9px; color:var(--text-secondary); white-space:nowrap; }
101
+ .typing-indicator { display:flex; align-items:center; gap:10px; padding:10px 16px; background:var(--bg-secondary); border:1px solid var(--border); border-left:3px solid var(--accent-gold); border-radius:4px; align-self:flex-start; width:fit-content; }
102
+ .dots { display:flex; gap:4px; align-items:center; }
103
+ .dot { width:5px; height:5px; border-radius:50%; background:var(--accent-gold); animation:blink 1.2s ease-in-out infinite; }
104
+ .dot:nth-child(2){animation-delay:0.2s} .dot:nth-child(3){animation-delay:0.4s}
105
+ .typing-label { font-size:11px; color:var(--text-secondary); font-style:italic; }
106
+ .typing-fun { font-size:10px; color:#e8002d; font-family:'JetBrains Mono',monospace; margin-top:6px; font-style:italic; opacity:0.9; }
107
+ @keyframes blink { 0%,80%,100%{opacity:0.2;transform:scale(0.8)} 40%{opacity:1;transform:scale(1.1)} }
108
+ #suggestions { display:flex; flex-wrap:wrap; gap:8px; padding:12px 28px; border-top:1px solid var(--border); background:var(--bg-primary); transition:opacity 0.3s,max-height 0.3s; overflow:hidden; max-height:60px; }
109
+ #suggestions.hidden { opacity:0; max-height:0; padding:0 28px; }
110
+ .sugg { background:transparent; border:1px solid var(--border); color:var(--text-secondary); font-size:12px; font-family:'Inter',sans-serif; padding:5px 12px; border-radius:3px; cursor:pointer; transition:all 120ms; white-space:nowrap; }
111
+ .sugg:hover { border-color:var(--accent-gold); color:var(--accent-gold-l); background:rgba(201,168,76,0.05); }
112
+ #input-bar { display:flex; align-items:center; gap:8px; padding:12px 20px; background:var(--bg-secondary); border-top:1px solid var(--border); }
113
+ #mic-btn { width:44px; height:44px; border-radius:50%; background:var(--bg-tertiary); border:1px solid var(--border); color:var(--text-secondary); font-size:18px; cursor:pointer; display:flex; align-items:center; justify-content:center; flex-shrink:0; transition:all 120ms; position:relative; }
114
+ #mic-btn:hover { border-color:var(--accent-red); color:var(--accent-red); }
115
+ #mic-btn.active { background:var(--accent-red); border-color:var(--accent-red); color:white; animation:mic-pulse 1.3s ease-in-out infinite; }
116
+ #mic-btn.active::after { content:''; position:absolute; inset:-7px; border-radius:50%; border:2px solid var(--accent-red); animation:ring-out 1.3s ease-out infinite; }
117
+ @keyframes mic-pulse { 0%,100%{box-shadow:0 0 0 0 rgba(232,0,45,0.5)} 50%{box-shadow:0 0 0 6px rgba(232,0,45,0)} }
118
+ @keyframes ring-out { 0%{transform:scale(1);opacity:0.7} 100%{transform:scale(1.7);opacity:0} }
119
+ #text-input { flex:1; background:var(--bg-tertiary); border:1px solid var(--border); border-radius:4px; color:var(--text-primary); font-family:'Inter',sans-serif; font-size:14px; padding:10px 14px; outline:none; transition:border-color 120ms; height:44px; }
120
+ #text-input:focus { border-color:var(--accent-red); box-shadow:0 0 0 2px rgba(232,0,45,0.1); }
121
+ #text-input::placeholder { color:var(--text-secondary); font-size:13px; }
122
+ #send-btn { height:44px; background:var(--accent-red); color:white; border:none; border-radius:4px; font-family:'Rajdhani',sans-serif; font-size:13px; font-weight:600; letter-spacing:2px; text-transform:uppercase; padding:0 22px; cursor:pointer; transition:all 120ms; white-space:nowrap; flex-shrink:0; }
123
+ #send-btn:hover { background:#ff1a42; box-shadow:0 2px 14px rgba(232,0,45,0.3); transform:translateY(-1px); }
124
+ #sidebar { width:240px; min-width:240px; background:var(--bg-secondary); padding:20px 16px; display:flex; flex-direction:column; gap:18px; overflow-y:auto; border-left:1px solid var(--border); }
125
+ #sidebar::-webkit-scrollbar{width:2px} #sidebar::-webkit-scrollbar-thumb{background:var(--border)}
126
+ .s-status { display:flex; align-items:center; gap:7px; font-family:'JetBrains Mono',monospace; font-size:9px; color:var(--text-secondary); letter-spacing:1.5px; text-transform:uppercase; }
127
+ .dot-live { width:5px; height:5px; border-radius:50%; background:#00c851; box-shadow:0 0 6px rgba(0,200,81,0.8); flex-shrink:0; }
128
+ .s-divider { height:1px; background:var(--border); }
129
+ .s-title { font-family:'Rajdhani',sans-serif; font-size:10px; font-weight:600; letter-spacing:2.5px; text-transform:uppercase; color:var(--text-secondary); margin-bottom:8px; }
130
+ .s-select { width:100%; background:var(--bg-tertiary); border:1px solid var(--border); border-radius:3px; color:var(--text-primary); font-size:13px; font-family:'Inter',sans-serif; padding:8px 10px; outline:none; cursor:pointer; margin-bottom:8px; appearance:none; background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' fill='none'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%23555' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E"); background-repeat:no-repeat; background-position:right 10px center; }
131
+ .s-select:focus { border-color:var(--accent-gold); }
132
+ .s-label { font-size:10px; color:var(--text-secondary); letter-spacing:1px; text-transform:uppercase; display:block; margin-bottom:5px; }
133
+ .s-checkbox-row { display:flex; align-items:center; gap:8px; cursor:pointer; margin-bottom:8px; }
134
+ .s-checkbox-row input[type="checkbox"] { accent-color:var(--accent-gold); width:14px; height:14px; cursor:pointer; }
135
+ .s-checkbox-row span { font-size:12px; color:var(--text-primary); }
136
+ .s-about { background:var(--bg-tertiary); border:1px solid var(--border); border-radius:4px; padding:14px; font-size:11px; color:var(--text-secondary); line-height:1.8; flex:1; }
137
+ .s-about-title { font-family:'JetBrains Mono',monospace; font-size:9px; color:var(--accent-gold); letter-spacing:2px; display:block; margin-bottom:10px; }
138
+ .s-source { color: var(--accent-gold); font-size:10px; font-family:'JetBrains Mono',monospace; letter-spacing:0.5px; text-decoration:none; }
139
+ .s-source:hover { text-decoration:underline; }
140
+ #rec-overlay { position:fixed; bottom:80px; left:50%; transform:translateX(-50%) translateY(10px); background:var(--bg-secondary); border:1px solid var(--accent-red); border-radius:4px; padding:10px 20px; display:flex; align-items:center; gap:10px; font-size:12px; color:var(--text-secondary); letter-spacing:1px; opacity:0; pointer-events:none; transition:all 200ms ease; z-index:100; box-shadow:0 4px 20px rgba(232,0,45,0.15); }
141
+ #rec-overlay.visible { opacity:1; transform:translateX(-50%) translateY(0); pointer-events:auto; }
142
+ .rec-dot { width:7px; height:7px; border-radius:50%; background:var(--accent-red); animation:blink-dot 0.8s ease-in-out infinite alternate; }
143
+ @keyframes blink-dot { from{opacity:1} to{opacity:0.2} }
144
+ #transcription-toast { position:fixed; top:70px; right:28px; background:var(--bg-secondary); border:1px solid var(--accent-gold); border-radius:4px; padding:10px 16px; font-size:12px; color:var(--text-secondary); letter-spacing:0.5px; opacity:0; transform:translateX(10px); transition:all 250ms ease; z-index:100; max-width:280px; }
145
+ #transcription-toast.visible { opacity:1; transform:translateX(0); }
146
+ .toast-label { font-family:'JetBrains Mono',monospace; font-size:9px; color:var(--accent-gold); letter-spacing:2px; text-transform:uppercase; display:block; margin-bottom:4px; }
147
+ </style>
148
+ </head>
149
+ <body>
150
+ <div id="header">
151
+ <div class="logo">🇲🇨 🎬 🎭 🖼️ <em>Monaco</em> Cultural Agent</div>
152
+ <div class="sep"></div>
153
+ <div class="tagline" id="last-scraped-line">Last updated: …</div>
154
+ <div class="header-right">
155
+ <div class="badge">Mistral Hackathon 2026</div>
156
+ <button type="button" id="theme-btn" title="Toggle light/dark theme">☀️</button>
157
+ </div>
158
+ </div>
159
+ <div id="app">
160
+ <div id="chat-area">
161
+ <div id="messages"></div>
162
+ <div id="suggestions">
163
+ <button type="button" class="sugg" data-sugg="What films are showing in Monaco this weekend?">🎬 Films this weekend</button>
164
+ <button type="button" class="sugg" data-sugg="What exhibitions are currently on in Monaco?">🖼️ Current exhibitions</button>
165
+ <button type="button" class="sugg" data-sugg="What is the Grimaldi Forum schedule?">🎤 Grimaldi Forum schedule</button>
166
+ <button type="button" class="sugg" data-sugg="What is the programme at the Théâtre Princesse Grace?">🎭 Théâtre Princesse Grace</button>
167
+ </div>
168
+ <div id="input-bar">
169
+ <button type="button" id="mic-btn" title="Voice recording">🎙</button>
170
+ <input type="text" id="text-input" placeholder="Ask your question about Monaco…" />
171
+ <button type="button" id="send-btn">Send</button>
172
+ </div>
173
+ </div>
174
+ <div id="sidebar">
175
+ <div class="s-status"><div class="dot-live"></div>Agent active</div>
176
+ <div class="s-divider"></div>
177
+ <div>
178
+ <div class="s-title">Model</div>
179
+ <span class="s-label">Provider</span>
180
+ <select id="provider-select" class="s-select"></select>
181
+ <span class="s-label">Model</span>
182
+ <select id="model-select" class="s-select"></select>
183
+ </div>
184
+ <div class="s-divider"></div>
185
+ <div>
186
+ <div class="s-title">ElevenLabs Voice</div>
187
+ <label class="s-checkbox-row">
188
+ <input type="checkbox" id="speaker-chk" />
189
+ <span>Auto voice response</span>
190
+ </label>
191
+ <span class="s-label">Voice</span>
192
+ <select id="voice-select" class="s-select"></select>
193
+ </div>
194
+ <div class="s-divider"></div>
195
+ <div class="s-about">
196
+ <span class="s-about-title">// ABOUT</span>
197
+ Specialized agent for cultural events in the Principality of Monaco. Areas of expertise: cultural events, exhibitions, shows, theatre, cinema, museums, opera, exotic garden, etc.<br><br>
198
+ <span style="color:#333;font-size:10px;font-family:'JetBrains Mono',monospace">Sources:</span><br>
199
+ <a class="s-source" href="https://www.oceano.mc" target="_blank">oceano.mc</a><br>
200
+ <a class="s-source" href="https://www.grimaldiforum.com" target="_blank">grimaldiforum.com</a><br>
201
+ <a class="s-source" href="https://www.cinemas2monaco.com" target="_blank">cinemas2monaco.com</a><br>
202
+ <a class="s-source" href="https://www.mediatheque.mc" target="_blank">mediatheque.mc</a><br>
203
+ <a class="s-source" href="https://www.letheatredesmuses.com" target="_blank">letheatredesmuses.com</a><br>
204
+ <a class="s-source" href="https://www.tpgmonaco.mc" target="_blank">tpgmonaco.mc</a>
205
+ </div>
206
+ </div>
207
+ </div>
208
+ <div id="rec-overlay"><div class="rec-dot"></div>Recording in progress… Click ⏹ to send</div>
209
+ <div id="transcription-toast"><span class="toast-label">// Voxtral</span><span id="toast-text"></span></div>
210
+ <script>
211
+ (function() {
212
+ const MODELS = {
213
+ "mistral": ["ministral-8b-latest", "mistral-large-latest", "mistral-small-latest"],
214
+ "vllm": [],
215
+ "nvidia": ["mistralai/ministral-14b-instruct-2512", "mistralai/mistral-large-3-675b-instruct-2512"],
216
+ "lmstudio": ["mistralai/ministral-14b-instruct-2512"],
217
+ };
218
+ const PROVIDERS = ["mistral", "nvidia", "lmstudio"];
219
+
220
+ const state = {
221
+ history: [],
222
+ provider: "mistral",
223
+ model: "ministral-8b-latest",
224
+ voiceId: "",
225
+ speakerAuto: false,
226
+ recording: false,
227
+ mediaRecorder: null,
228
+ audioChunks: [],
229
+ };
230
+
231
+ const messagesEl = document.getElementById("messages");
232
+ const suggestionsEl = document.getElementById("suggestions");
233
+ const textInput = document.getElementById("text-input");
234
+ const sendBtn = document.getElementById("send-btn");
235
+ const micBtn = document.getElementById("mic-btn");
236
+ const providerSelect = document.getElementById("provider-select");
237
+ const modelSelect = document.getElementById("model-select");
238
+ const voiceSelect = document.getElementById("voice-select");
239
+ const speakerChk = document.getElementById("speaker-chk");
240
+ const recOverlay = document.getElementById("rec-overlay");
241
+ const toastEl = document.getElementById("transcription-toast");
242
+ const toastText = document.getElementById("toast-text");
243
+
244
+ function escapeHtml(s) {
245
+ const div = document.createElement("div");
246
+ div.textContent = s;
247
+ return div.innerHTML;
248
+ }
249
+
250
+ function stripUrlsForTTS(txt) {
251
+ if (!txt || !txt.trim()) return "";
252
+ return txt.replace(/\s*https?:\/\/[^\s]+\s*/gi, " ").replace(/\s+/g, " ").trim();
253
+ }
254
+
255
+ function getDomainFromUrl(url) {
256
+ try {
257
+ return url.replace(/^https?:\/\/(?:www\.)?/, "").split(/[/?#]/)[0] || url;
258
+ } catch (err) { return url; }
259
+ }
260
+
261
+ function renderMarkup(txt) {
262
+ if (!txt) return "";
263
+ return escapeHtml(txt)
264
+ .replace(/\n/g, "<br>")
265
+ .replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
266
+ .replace(/^- (.+)$/gm, "<li>$1</li>")
267
+ .replace(/(<li>.*<\/li>)/s, "<ul>$1</ul>");
268
+ }
269
+
270
+ function formatIntroHtml(intro) {
271
+ if (!intro || !intro.trim()) return "";
272
+ var escaped = escapeHtml(intro);
273
+ escaped = escaped.replace(/\*\*(.+?)\*\*/g, '<span class="event-title">$1</span>');
274
+ escaped = escaped.replace(/\[([^\]]*)\]\((https?:\S+)\)/g, function(_, __, url) {
275
+ var domain = getDomainFromUrl(url);
276
+ return '<a class="event-link" href="' + escapeHtml(url) + '" target="_blank" rel="noopener">' + escapeHtml(domain) + " →</a>";
277
+ });
278
+ return escaped;
279
+ }
280
+
281
+ function formatItemHtml(line) {
282
+ var escaped = escapeHtml(line);
283
+ escaped = escaped.replace(/\*\*(.+?)\*\*/g, '<span class="event-title">$1</span>');
284
+ escaped = escaped.replace(/\[([^\]]*)\]\((https?:\S+)\)/g, function(_, __, url) {
285
+ var domain = getDomainFromUrl(url);
286
+ return '<a class="event-link" href="' + escapeHtml(url) + '" target="_blank" rel="noopener">' + escapeHtml(domain) + " →</a>";
287
+ });
288
+ return escaped;
289
+ }
290
+
291
+ function parseEventLine(line) {
292
+ line = line.replace(/\s+/g, " ").trim();
293
+ var title = "", date = "", lieu = "", tarif = "", url = "";
294
+ var lieuM = line.match(/\b(?:Lieu|Venue)\s*:\s*([^.]*?)(?=\.\s*(?:Tarif|Price|Lien|Link|Horaires|Schedule)|\.\s*$|$)/i);
295
+ var tarifM = line.match(/\b(?:Tarif|Price)\s*:\s*([^.]*?)(?=\.\s*(?:Lien|Link)|\.\s*$|$)/i);
296
+ if (!tarifM && /(?:Gratuit|Free)/i.test(line)) tarifM = [null, (line.match(/(?:Gratuit|Free)[^.]*\.?/) || [])[0] || "Free"];
297
+ var lienMd = line.match(/\b(?:Lien|Link)\s*:\s*\[[^\]]*\]\((https?:[^)]+)\)/i);
298
+ var lienRaw = line.match(/\b(?:Lien|Link)\s*:\s*(https?:\S+)/i);
299
+ var lienBare = line.match(/\b(?:Lien|Link)\s*:\s*(\S+)/i);
300
+ var duM = line.match(/(?:Du|From)\s+(.+?)\s+(?:au|to)\s+([^.]*?)(?=\.|$)/i);
301
+ if (lieuM) lieu = lieuM[1].trim();
302
+ if (tarifM) tarif = tarifM[1].trim();
303
+ if (lienMd) url = lienMd[1].trim();
304
+ else if (lienRaw) url = lienRaw[1].trim();
305
+ else if (lienBare) { var u = lienBare[1].trim().replace(/^\[|\]\.?$/g, ""); if (u) url = u.indexOf("http") === 0 ? u : "https://" + u; }
306
+ if (duM) date = (duM[1].trim() + " – " + duM[2].trim()).replace(/\s*\.\s*$/, "");
307
+ var beforeLieu = lieuM ? line.substring(0, line.indexOf(lieuM[0])).trim() : line;
308
+ title = beforeLieu.split(". ")[0].trim();
309
+ var duIdx = Math.max(beforeLieu.indexOf("Du "), beforeLieu.indexOf("From "));
310
+ if (duM && duIdx >= 0 && title.length > duIdx) {
311
+ var avantDu = beforeLieu.substring(0, duIdx).trim();
312
+ if (avantDu) title = avantDu.replace(/\s*\.\s*$/, "");
313
+ }
314
+ if (!title) title = beforeLieu.split(". ")[0] || beforeLieu;
315
+ return { title: title, date: date, lieu: lieu, tarif: tarif || "Price not available", url: url };
316
+ }
317
+
318
+ function looksLikeEventList(intro, firstItem) {
319
+ if (!firstItem || !intro) return false;
320
+ var lower = (intro + " " + firstItem).toLowerCase();
321
+ if (lower.indexOf("lieu") >= 0 || lower.indexOf("venue") >= 0 || lower.indexOf("du ") >= 0 || lower.indexOf("from ") >= 0) return true;
322
+ if (/^\d+\.\s*.{10,}/.test(firstItem) && (firstItem.indexOf("Lieu") >= 0 || firstItem.indexOf("Venue") >= 0 || firstItem.indexOf("Tarif") >= 0 || firstItem.indexOf("Price") >= 0)) return true;
323
+ return false;
324
+ }
325
+
326
+ function eventEmojiFromIntro(intro) {
327
+ var i = (intro || "").toLowerCase();
328
+ if (i.indexOf("concert") >= 0) return "🎵";
329
+ if (i.indexOf("exposition") >= 0 || i.indexOf("exhibition") >= 0 || i.indexOf("expo") >= 0) return "🖼️";
330
+ if (i.indexOf("humour") >= 0 || i.indexOf("comedy") >= 0) return "🎤";
331
+ if (i.indexOf("atelier") >= 0 || i.indexOf("workshop") >= 0) return "📚";
332
+ if (i.indexOf("théâtre") >= 0 || i.indexOf("theatre") >= 0 || i.indexOf("theater") >= 0 || i.indexOf("spectacle") >= 0 || i.indexOf("show") >= 0) return "🎭";
333
+ return "🖼️";
334
+ }
335
+
336
+ function buildShortTTS(data) {
337
+ var response = (data && data.response) || "";
338
+ var events = data && data.events && Array.isArray(data.events) ? data.events : null;
339
+ if (data && data.tts_text && (data.tts_text + "").trim()) return (data.tts_text + "").trim();
340
+ if (events && events.length > 0) {
341
+ var intro = (response || "").split("\n\n")[0] || "";
342
+ var titres = events.map(function(e) { return (e.titre || "").trim(); }).filter(Boolean);
343
+ return (intro.trim() + (titres.length ? " " + titres.join(", ") : "")).trim() || response;
344
+ }
345
+ var parts = (response || "").trim().split(/\n\n+/);
346
+ var intro = parts[0] || "";
347
+ var body = parts.slice(1).join("\n\n").trim();
348
+ var items = body ? body.split(/(?=^\d+\.\s)/m).filter(function(s) { return /^\d+\.\s/.test(s.trim()); }) : [];
349
+ if (items.length > 0) {
350
+ var firstLine = items[0].replace(/^\d+\.\s*/, "").trim();
351
+ if (looksLikeEventList(intro, firstLine)) {
352
+ var titres = items.map(function(it) {
353
+ var line = it.replace(/^\d+\.\s*/, "").trim();
354
+ var p = parseEventLine(line);
355
+ return p.title || line.split(". ")[0] || line;
356
+ }).filter(Boolean);
357
+ return (intro.trim() + (titres.length ? " " + titres.join(", ") : "")).trim() || stripUrlsForTTS(response);
358
+ }
359
+ }
360
+ return stripUrlsForTTS(response);
361
+ }
362
+
363
+ function renderAgentResponse(txt) {
364
+ if (!txt || !txt.trim()) return "";
365
+ var parts = txt.trim().split(/\n\n+/);
366
+ var intro = parts[0] || "";
367
+ var body = parts.slice(1).join("\n\n").trim();
368
+ var introHtml = "<p class=\"agent-intro\">" + formatIntroHtml(intro) + "</p>";
369
+ if (!body.trim()) return introHtml;
370
+ var items = body.split(/(?=^\d+\.\s)/m).filter(function(s) { return /^\d+\.\s/.test(s.trim()); });
371
+ var listHtml = "";
372
+ if (items.length > 0) {
373
+ var firstLine = items[0].replace(/^\d+\.\s*/, "").trim();
374
+ if (looksLikeEventList(intro, firstLine)) {
375
+ var emoji = eventEmojiFromIntro(intro);
376
+ listHtml = '<ul class="event-list">';
377
+ items.forEach(function(it) {
378
+ var line = it.replace(/^\d+\.\s*/, "").trim();
379
+ var p = parseEventLine(line);
380
+ if (!p.title) p.title = line.split(". ")[0] || line;
381
+ listHtml += '<li class="event-item">';
382
+ listHtml += '<span class="event-title">' + emoji + " " + escapeHtml(p.title) + "</span>";
383
+ listHtml += '<div class="event-meta">';
384
+ listHtml += (p.date ? "📅 " + escapeHtml(p.date) + " . " : "") + "📍 " + escapeHtml(p.lieu);
385
+ if (p.tarif && p.tarif !== "Price not available") listHtml += " . 🎟️ " + escapeHtml(p.tarif);
386
+ listHtml += "</div>";
387
+ if (p.url) listHtml += '<a class="event-link" href="' + escapeHtml(p.url) + '" target="_blank" rel="noopener">' + escapeHtml(getDomainFromUrl(p.url)) + " →</a>";
388
+ listHtml += "</li>";
389
+ });
390
+ listHtml += "</ul>";
391
+ } else {
392
+ listHtml = '<ul class="event-list">';
393
+ items.forEach(function(it) {
394
+ var line = it.replace(/^\d+\.\s*/, "").trim();
395
+ listHtml += '<li class="event-item">' + formatItemHtml(line).replace(/\n/g, "<br>") + "</li>";
396
+ });
397
+ listHtml += "</ul>";
398
+ }
399
+ } else {
400
+ listHtml = '<div class="agent-body event-meta">' + formatItemHtml(body).replace(/\n/g, "<br>") + "</div>";
401
+ }
402
+ return introHtml + listHtml;
403
+ }
404
+
405
+ function appendMessage(role, content, isVocal, events) {
406
+ const wrap = document.createElement("div");
407
+ wrap.className = "msg-wrap";
408
+ const isUser = role === "user";
409
+ const label = document.createElement("div");
410
+ label.className = "msg-label " + (isUser ? "user-label" : "agent-label");
411
+ label.textContent = isUser ? (isVocal ? "YOU · 🎙 VOICE" : "YOU") : "AGENT";
412
+ const msg = document.createElement("div");
413
+ msg.className = "msg " + (isUser ? "user" : "agent");
414
+ if (!isUser && events && events.length > 0) {
415
+ var intro = (content || "").split("\n\n")[0] || "";
416
+ msg.innerHTML = renderEventsBlock(intro, events);
417
+ } else if (!isUser && content) {
418
+ msg.innerHTML = renderAgentResponse(content);
419
+ } else {
420
+ msg.innerHTML = renderMarkup(content);
421
+ }
422
+ wrap.appendChild(label);
423
+ wrap.appendChild(msg);
424
+ messagesEl.appendChild(wrap);
425
+ messagesEl.scrollTop = messagesEl.scrollHeight;
426
+ return wrap;
427
+ }
428
+
429
+ function eventKindFromTags(tags) {
430
+ var t = (tags || []).map(function(x) { return (x || "").toLowerCase(); });
431
+ if (t.some(function(x) { return x.indexOf("concert") >= 0 || x.indexOf("musique") >= 0; })) return "concert";
432
+ if (t.some(function(x) { return x.indexOf("film") >= 0 || x.indexOf("cinéma") >= 0 || x.indexOf("cinema") >= 0 || x.indexOf("projection") >= 0; })) return "film";
433
+ if (t.some(function(x) { return x.indexOf("expo") >= 0 || x.indexOf("exposition") >= 0; })) return "exposition";
434
+ if (t.some(function(x) { return x.indexOf("humour") >= 0; })) return "humour";
435
+ if (t.some(function(x) { return x.indexOf("atelier") >= 0; })) return "atelier";
436
+ if (t.some(function(x) { return x.indexOf("théâtre") >= 0 || x.indexOf("theatre") >= 0 || x.indexOf("spectacle") >= 0; })) return "theatre";
437
+ return "default";
438
+ }
439
+
440
+ var MOIS = ["January","February","March","April","May","June","July","August","September","October","November","December"];
441
+ function formatDateShort(startStr, endStr) {
442
+ if (!startStr) return "";
443
+ var d = startStr.split("-");
444
+ var j = d[2] ? parseInt(d[2], 10) : "";
445
+ var m = d[1] ? MOIS[parseInt(d[1], 10) - 1] : "";
446
+ if (!endStr || endStr === startStr) return (j && m) ? j + " " + m : startStr;
447
+ var e = endStr.split("-");
448
+ var j2 = e[2] ? parseInt(e[2], 10) : "";
449
+ var m2 = e[1] ? MOIS[parseInt(e[1], 10) - 1] : "";
450
+ if (m2 === m) return (j && j2 && m) ? j + "-" + j2 + " " + m : startStr + "–" + endStr;
451
+ return (j && m && j2 && m2) ? j + " " + m + " – " + j2 + " " + m2 : startStr + "–" + endStr;
452
+ }
453
+
454
+ function renderEventsBlock(intro, events) {
455
+ var html = "";
456
+ if (intro) html += '<p class="agent-intro">' + escapeHtml(intro.trim()) + "</p>";
457
+ html += '<ul class="event-list">';
458
+ events.forEach(function(e) {
459
+ var titre = e.titre || "";
460
+ var lieu = e.lieu_nom || "";
461
+ var start = e.date_start || "";
462
+ var end = e.date_end || e.date_start || "";
463
+ var heure = e.heure_debut || "";
464
+ var description = (e.description || "").trim();
465
+ var kind = eventKindFromTags(e.tags);
466
+ var tarif = e.tarif ? (e.tarif + "").trim() : "";
467
+ if (!tarif && e.gratuit) tarif = "Free admission";
468
+ if (!tarif) tarif = "Price not available";
469
+ var linkUrl = (e.url != null && e.url !== "") ? String(e.url).trim() : "";
470
+ if (!linkUrl && e.source) linkUrl = (e.source != null && e.source !== "") ? String(e.source).trim() : "";
471
+ var domain = linkUrl ? getDomainFromUrl(linkUrl) : "";
472
+ var emoji = "🎭";
473
+ if (kind === "concert") emoji = "🎵";
474
+ else if (kind === "film") emoji = "🎬";
475
+ else if (kind === "exposition") emoji = "🖼️";
476
+ else if (kind === "humour") emoji = "🎤";
477
+ else if (kind === "atelier") emoji = "📚";
478
+ else if (kind === "theatre") emoji = "🎭";
479
+ var metaHtml = "";
480
+ if (kind === "film") {
481
+ var metaParts = [];
482
+ if (heure || (description && (description.indexOf("h") >= 0 || description.indexOf(":") >= 0))) {
483
+ if (description && description.length < 80) metaParts.push("🕐 " + escapeHtml(description));
484
+ else if (heure) metaParts.push("🕐 From " + escapeHtml(heure));
485
+ }
486
+ metaParts.push("🗓️ " + escapeHtml(start + (end && end !== start ? "–" + end : "")));
487
+ metaParts.push("📍 " + escapeHtml(lieu));
488
+ if (tarif !== "Price not available") metaParts.push("💰 " + escapeHtml(tarif));
489
+ metaHtml = metaParts.join(" • ");
490
+ } else {
491
+ var dateLabel = formatDateShort(start, end);
492
+ metaHtml = "📅 " + escapeHtml(dateLabel) + " . 📍 " + escapeHtml(lieu);
493
+ if (tarif !== "Price not available") metaHtml += " . 🎟️ " + escapeHtml(tarif);
494
+ }
495
+ html += '<li class="event-item">';
496
+ html += '<span class="event-title">' + emoji + " " + escapeHtml(titre) + "</span>";
497
+ html += '<div class="event-meta">' + metaHtml + "</div>";
498
+ if (linkUrl && domain) html += '<a class="event-link" href="' + escapeHtml(linkUrl) + '" target="_blank" rel="noopener">' + escapeHtml(domain) + " →</a>";
499
+ html += "</li>";
500
+ });
501
+ html += "</ul>";
502
+ return html;
503
+ }
504
+
505
+ function formatTime(sec) {
506
+ if (!isFinite(sec) || sec < 0) return "0:00";
507
+ var m = Math.floor(sec / 60);
508
+ var s = Math.floor(sec % 60);
509
+ return m + ":" + (s < 10 ? "0" : "") + s;
510
+ }
511
+
512
+ function attachAudioFromBlob(audioDiv, blob) {
513
+ var playBtn = audioDiv.querySelector(".play-btn");
514
+ var progress = audioDiv.querySelector(".audio-progress");
515
+ var timeEl = audioDiv.querySelector(".audio-time");
516
+ var url = URL.createObjectURL(blob);
517
+ var audio = new Audio(url);
518
+ audioDiv._audio = audio;
519
+ audio.onended = function() {
520
+ playBtn.innerHTML = "▶";
521
+ progress.style.width = "0%";
522
+ timeEl.textContent = "0:00 / " + formatTime(audio.duration);
523
+ playBtn.disabled = false;
524
+ URL.revokeObjectURL(url);
525
+ };
526
+ audio.onloadedmetadata = function() {
527
+ timeEl.textContent = "0:00 / " + formatTime(audio.duration);
528
+ };
529
+ audio.ontimeupdate = function() {
530
+ var p = audio.duration ? (audio.currentTime / audio.duration) * 100 : 0;
531
+ progress.style.width = p + "%";
532
+ timeEl.textContent = formatTime(audio.currentTime) + " / " + formatTime(audio.duration);
533
+ };
534
+ }
535
+
536
+ function addAudioPlayer(wrap, text, preloadedBlob) {
537
+ var msg = wrap.querySelector(".msg");
538
+ var audioDiv = msg.querySelector(".msg-audio");
539
+ if (audioDiv) return audioDiv;
540
+ audioDiv = document.createElement("div");
541
+ audioDiv.className = "msg-audio";
542
+ var playBtn = document.createElement("button");
543
+ playBtn.type = "button";
544
+ playBtn.className = "play-btn";
545
+ playBtn.innerHTML = "▶";
546
+ playBtn.title = "Listen";
547
+ var barWrap = document.createElement("div");
548
+ barWrap.className = "audio-bar";
549
+ var progress = document.createElement("div");
550
+ progress.className = "audio-progress";
551
+ barWrap.appendChild(progress);
552
+ var timeEl = document.createElement("span");
553
+ timeEl.className = "audio-time";
554
+ timeEl.textContent = "0:00 / 0:00";
555
+ audioDiv.appendChild(playBtn);
556
+ audioDiv.appendChild(barWrap);
557
+ audioDiv.appendChild(timeEl);
558
+ msg.appendChild(audioDiv);
559
+
560
+ if (preloadedBlob) attachAudioFromBlob(audioDiv, preloadedBlob);
561
+
562
+ playBtn.addEventListener("click", function() {
563
+ if (audioDiv._audio) {
564
+ if (audioDiv._audio.paused) {
565
+ audioDiv._audio.play();
566
+ playBtn.innerHTML = "⏸";
567
+ } else {
568
+ audioDiv._audio.pause();
569
+ playBtn.innerHTML = "▶";
570
+ }
571
+ return;
572
+ }
573
+ if (!(text || "").trim()) return;
574
+ playBtn.disabled = true;
575
+ playBtn.innerHTML = "";
576
+ playBtn.classList.add("loading");
577
+ fetch("/tts", {
578
+ method: "POST",
579
+ headers: { "Content-Type": "application/json" },
580
+ body: JSON.stringify({ text: text.trim(), voice_id: state.voiceId || null }),
581
+ })
582
+ .then(function(r) {
583
+ if (!r.ok) throw new Error("TTS failed");
584
+ return r.blob();
585
+ })
586
+ .then(function(blob) {
587
+ playBtn.classList.remove("loading");
588
+ attachAudioFromBlob(audioDiv, blob);
589
+ audioDiv._audio.play().then(function() {
590
+ playBtn.innerHTML = "⏸";
591
+ playBtn.disabled = false;
592
+ }).catch(function() {
593
+ playBtn.innerHTML = "▶";
594
+ playBtn.disabled = false;
595
+ });
596
+ })
597
+ .catch(function() {
598
+ playBtn.classList.remove("loading");
599
+ playBtn.innerHTML = "▶";
600
+ playBtn.disabled = false;
601
+ });
602
+ });
603
+ return audioDiv;
604
+ }
605
+
606
+ function playTTSForMessage(wrap, text) {
607
+ if (!(text || "").trim()) return;
608
+ addAudioPlayer(wrap, text);
609
+ var audioDiv = wrap.querySelector(".msg-audio");
610
+ var playBtn = audioDiv.querySelector(".play-btn");
611
+ if (audioDiv._audio) {
612
+ audioDiv._audio.currentTime = 0;
613
+ audioDiv._audio.play();
614
+ playBtn.innerHTML = "⏸";
615
+ return;
616
+ }
617
+ playBtn.disabled = true;
618
+ playBtn.innerHTML = "";
619
+ playBtn.classList.add("loading");
620
+ fetch("/tts", {
621
+ method: "POST",
622
+ headers: { "Content-Type": "application/json" },
623
+ body: JSON.stringify({ text: text.trim(), voice_id: state.voiceId || null }),
624
+ })
625
+ .then(function(r) {
626
+ if (!r.ok) throw new Error("TTS failed");
627
+ return r.blob();
628
+ })
629
+ .then(function(blob) {
630
+ playBtn.classList.remove("loading");
631
+ var url = URL.createObjectURL(blob);
632
+ var audio = new Audio(url);
633
+ audioDiv._audio = audio;
634
+ var progress = audioDiv.querySelector(".audio-progress");
635
+ var timeEl = audioDiv.querySelector(".audio-time");
636
+ audio.onended = function() {
637
+ playBtn.innerHTML = "▶";
638
+ progress.style.width = "0%";
639
+ timeEl.textContent = "0:00 / " + formatTime(audio.duration);
640
+ playBtn.disabled = false;
641
+ URL.revokeObjectURL(url);
642
+ };
643
+ audio.onloadedmetadata = function() {
644
+ timeEl.textContent = "0:00 / " + formatTime(audio.duration);
645
+ };
646
+ audio.ontimeupdate = function() {
647
+ var p = audio.duration ? (audio.currentTime / audio.duration) * 100 : 0;
648
+ progress.style.width = p + "%";
649
+ timeEl.textContent = formatTime(audio.currentTime) + " / " + formatTime(audio.duration);
650
+ };
651
+ audio.play().then(function() {
652
+ playBtn.innerHTML = "⏸";
653
+ playBtn.disabled = false;
654
+ }).catch(function() {
655
+ playBtn.innerHTML = "▶";
656
+ playBtn.disabled = false;
657
+ });
658
+ })
659
+ .catch(function() {
660
+ playBtn.classList.remove("loading");
661
+ playBtn.innerHTML = "▶";
662
+ playBtn.disabled = false;
663
+ });
664
+ }
665
+
666
+ function guessFrontLang(text) {
667
+ var t = (text || "").toLowerCase();
668
+ if (/\b(what|where|when|how|show|is|are|the|and|this|week)\b/.test(t)) return "en";
669
+ if (/\b(cosa|dove|quando|come|spettacolo|mostra|questa)\b/.test(t)) return "it";
670
+ if (/\b(qué|dónde|cuándo|cómo|espectáculo|esta|semana)\b/.test(t)) return "es";
671
+ if (/[а-яёА-ЯЁ]/.test(t)) return "ru";
672
+ return "fr";
673
+ }
674
+
675
+ var FUN_FACTS = {
676
+ fr: [
677
+ "Monaco compte environ 38 000 habitants pour seulement 2,02 km² — la ville la plus dense du monde.",
678
+ "Monaco est le 2ème plus petit État souverain du monde, après le Vatican.",
679
+ "Le théâtre se dit « teatru » en langue monégasque. 🎭",
680
+ ],
681
+ en: [
682
+ "Monaco has around 38,000 inhabitants in just 2.02 km² — the world's most densely populated country.",
683
+ "Monaco is the 2nd smallest sovereign state in the world, after Vatican City.",
684
+ "The word for theatre in Monégasque, the local language, is « teatru ». 🎭",
685
+ ],
686
+ it: [
687
+ "Monaco ha circa 38.000 abitanti in soli 2,02 km² — il paese più densamente popolato al mondo.",
688
+ "Monaco è il 2° stato sovrano più piccolo del mondo, dopo il Vaticano.",
689
+ "In monegasco, il teatro si dice « teatru ». 🎭",
690
+ ],
691
+ es: [
692
+ "Mónaco tiene unos 38.000 habitantes en apenas 2,02 km² — el país más densamente poblado del mundo.",
693
+ "Mónaco es el 2º estado soberano más pequeño del mundo, después del Vaticano.",
694
+ "En monegasco, teatro se dice « teatru ». 🎭",
695
+ ],
696
+ ru: [
697
+ "В Монако около 38 000 жителей на площади всего 2,02 км² — самое густонаселённое государство мира.",
698
+ "Монако — 2-е по величине государство в мире после Ватикана.",
699
+ "На монегасском языке театр называется « teatru ». 🎭",
700
+ ],
701
+ };
702
+
703
+ var DID_YOU_KNOW = {
704
+ fr: "Le saviez-vous ?",
705
+ en: "Did you know?",
706
+ it: "Lo sapevi?",
707
+ es: "¿Sabías que?",
708
+ ru: "Знаете ли вы?",
709
+ };
710
+
711
+ function getDidYouKnow(lang) {
712
+ return DID_YOU_KNOW[lang] || DID_YOU_KNOW["fr"];
713
+ }
714
+
715
+ function getRandomFact(lang) {
716
+ var list = FUN_FACTS[lang] || FUN_FACTS["fr"];
717
+ return list[Math.floor(Math.random() * list.length)];
718
+ }
719
+
720
+ function showTyping(lang) {
721
+ var fact = getRandomFact(lang || state.lastLang || "fr");
722
+ var wrap = document.createElement("div");
723
+ wrap.className = "msg-wrap typing-wrap";
724
+ wrap.innerHTML = '<div class="typing-indicator" style="flex-direction:column;align-items:flex-start;">'
725
+ + '<div style="display:flex;align-items:center;gap:10px"><div class="dots"><span class="dot"></span><span class="dot"></span><span class="dot"></span></div><span class="typing-label">Agent searching…</span></div>'
726
+ + '<div class="typing-fun">💡 ' + getDidYouKnow(lang) + ' ' + fact + '</div>'
727
+ + '</div>';
728
+ messagesEl.appendChild(wrap);
729
+ messagesEl.scrollTop = messagesEl.scrollHeight;
730
+ return wrap;
731
+ }
732
+
733
+ function removeTyping(wrap) {
734
+ if (wrap && wrap.parentNode) wrap.parentNode.removeChild(wrap);
735
+ }
736
+
737
+ function showToast(text) {
738
+ toastText.textContent = text || "";
739
+ toastEl.classList.add("visible");
740
+ setTimeout(function() {
741
+ toastEl.classList.remove("visible");
742
+ }, 3500);
743
+ }
744
+
745
+ function sendMessage(message, isVocal) {
746
+ var text = (message || "").trim();
747
+ if (!text) return;
748
+ state.history.push({ role: "user", content: text });
749
+ appendMessage("user", text, !!isVocal);
750
+ suggestionsEl.classList.add("hidden");
751
+
752
+ var typingWrap = showTyping(guessFrontLang(text));
753
+ fetch("/chat", {
754
+ method: "POST",
755
+ headers: { "Content-Type": "application/json" },
756
+ body: JSON.stringify({
757
+ message: text,
758
+ history: state.history.slice(0, -1),
759
+ provider: state.provider,
760
+ model: state.model || null,
761
+ }),
762
+ })
763
+ .then(function(r) { return r.json(); })
764
+ .then(function(data) {
765
+ var response = (data && data.response) || "";
766
+ var events = data && data.events && Array.isArray(data.events) ? data.events : null;
767
+ var ttsText = buildShortTTS(data);
768
+ state.history.push({ role: "assistant", content: response });
769
+
770
+ function showMessage(blob) {
771
+ removeTyping(typingWrap);
772
+ var agentWrap = appendMessage("assistant", response, false, events);
773
+ addAudioPlayer(agentWrap, ttsText, blob || null);
774
+ if (state.speakerAuto || isVocal) {
775
+ var ad = agentWrap.querySelector(".msg-audio");
776
+ if (ad && ad._audio) {
777
+ ad._audio.play();
778
+ ad.querySelector(".play-btn").innerHTML = "⏸";
779
+ } else if (ad && ttsText) playTTSForMessage(agentWrap, ttsText);
780
+ }
781
+ }
782
+
783
+ if (!(ttsText && ttsText.trim()) || !state.speakerAuto) {
784
+ showMessage(null);
785
+ return;
786
+ }
787
+ fetch("/tts", {
788
+ method: "POST",
789
+ headers: { "Content-Type": "application/json" },
790
+ body: JSON.stringify({ text: ttsText.trim(), voice_id: state.voiceId || null }),
791
+ })
792
+ .then(function(r) {
793
+ if (!r.ok) throw new Error("TTS failed");
794
+ return r.blob();
795
+ })
796
+ .then(function(blob) { showMessage(blob); })
797
+ .catch(function() { showMessage(null); });
798
+ })
799
+ .catch(function() {
800
+ removeTyping(typingWrap);
801
+ state.history.push({ role: "assistant", content: "Sorry, an error occurred." });
802
+ appendMessage("assistant", "Sorry, an error occurred.", false);
803
+ });
804
+ }
805
+
806
+ function populateProviders() {
807
+ providerSelect.innerHTML = "";
808
+ PROVIDERS.forEach(function(p) {
809
+ const opt = document.createElement("option");
810
+ opt.value = p;
811
+ opt.textContent = p;
812
+ if (p === state.provider) opt.selected = true;
813
+ providerSelect.appendChild(opt);
814
+ });
815
+ }
816
+
817
+ function populateModels() {
818
+ const list = MODELS[state.provider] || [];
819
+ const prev = state.model;
820
+ modelSelect.innerHTML = "";
821
+ list.forEach(function(m) {
822
+ const opt = document.createElement("option");
823
+ opt.value = m;
824
+ opt.textContent = m;
825
+ if (m === prev || (!prev && list.length)) opt.selected = true;
826
+ modelSelect.appendChild(opt);
827
+ });
828
+ state.model = modelSelect.value || (list[0] || "");
829
+ }
830
+
831
+ function populateVoices() {
832
+ fetch("/voices")
833
+ .then(function(r) { return r.json(); })
834
+ .then(function(voices) {
835
+ voiceSelect.innerHTML = "";
836
+ const defOpt = document.createElement("option");
837
+ defOpt.value = "";
838
+ defOpt.textContent = "Default";
839
+ voiceSelect.appendChild(defOpt);
840
+ (voices || []).forEach(function(v) {
841
+ const opt = document.createElement("option");
842
+ opt.value = v.voice_id || "";
843
+ opt.textContent = v.name || v.voice_id || "";
844
+ voiceSelect.appendChild(opt);
845
+ });
846
+ if (voices && voices.length && !state.voiceId) state.voiceId = voices[0].voice_id || "";
847
+ })
848
+ .catch(function() {});
849
+ }
850
+
851
+ providerSelect.addEventListener("change", function() {
852
+ state.provider = providerSelect.value;
853
+ populateModels();
854
+ });
855
+
856
+ modelSelect.addEventListener("change", function() {
857
+ state.model = modelSelect.value;
858
+ });
859
+
860
+ voiceSelect.addEventListener("change", function() {
861
+ state.voiceId = voiceSelect.value;
862
+ });
863
+
864
+ speakerChk.addEventListener("change", function() {
865
+ state.speakerAuto = speakerChk.checked;
866
+ });
867
+
868
+ sendBtn.addEventListener("click", function() {
869
+ sendMessage(textInput.value, false);
870
+ textInput.value = "";
871
+ });
872
+
873
+ textInput.addEventListener("keydown", function(e) {
874
+ if (e.key === "Enter" && !e.shiftKey) {
875
+ e.preventDefault();
876
+ sendMessage(textInput.value, false);
877
+ textInput.value = "";
878
+ }
879
+ });
880
+
881
+ document.querySelectorAll(".sugg").forEach(function(btn) {
882
+ btn.addEventListener("click", function() {
883
+ const text = btn.getAttribute("data-sugg") || btn.textContent;
884
+ sendMessage(text, false);
885
+ });
886
+ });
887
+
888
+ micBtn.addEventListener("click", function() {
889
+ if (state.recording) {
890
+ state.recording = false;
891
+ micBtn.classList.remove("active");
892
+ micBtn.textContent = "🎙";
893
+ recOverlay.classList.remove("visible");
894
+ if (state.mediaRecorder && state.mediaRecorder.state !== "inactive") {
895
+ state.mediaRecorder.stop();
896
+ }
897
+ return;
898
+ }
899
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
900
+ return;
901
+ }
902
+ navigator.mediaDevices.getUserMedia({ audio: true })
903
+ .then(function(stream) {
904
+ state.audioChunks = [];
905
+ const mr = new MediaRecorder(stream);
906
+ state.mediaRecorder = mr;
907
+ mr.ondataavailable = function(e) {
908
+ if (e.data.size) state.audioChunks.push(e.data);
909
+ };
910
+ mr.onstop = function() {
911
+ stream.getTracks().forEach(function(t) { t.stop(); });
912
+ const blob = new Blob(state.audioChunks, { type: "audio/webm" });
913
+ const fd = new FormData();
914
+ fd.append("file", blob, "recording.webm");
915
+ fetch("/transcribe", { method: "POST", body: fd })
916
+ .then(function(r) { return r.json(); })
917
+ .then(function(data) {
918
+ const text = (data && data.text) || "";
919
+ if (!text) return;
920
+ showToast(text);
921
+ state.history.push({ role: "user", content: text });
922
+ appendMessage("user", text, true);
923
+ suggestionsEl.classList.add("hidden");
924
+ const typingWrap = showTyping(guessFrontLang(text));
925
+ fetch("/chat", {
926
+ method: "POST",
927
+ headers: { "Content-Type": "application/json" },
928
+ body: JSON.stringify({
929
+ message: text,
930
+ history: state.history.slice(0, -1),
931
+ provider: state.provider,
932
+ model: state.model || null,
933
+ }),
934
+ })
935
+ .then(function(res) { return res.json(); })
936
+ .then(function(chatData) {
937
+ var response = (chatData && chatData.response) || "";
938
+ var events = chatData && chatData.events && Array.isArray(chatData.events) ? chatData.events : null;
939
+ var ttsText = buildShortTTS(chatData);
940
+ state.history.push({ role: "assistant", content: response });
941
+
942
+ function showMessage(blob) {
943
+ removeTyping(typingWrap);
944
+ var agentWrap = appendMessage("assistant", response, false, events);
945
+ addAudioPlayer(agentWrap, ttsText, blob || null);
946
+ var ad = agentWrap.querySelector(".msg-audio");
947
+ if (ad && ad._audio) {
948
+ ad._audio.play();
949
+ ad.querySelector(".play-btn").innerHTML = "⏸";
950
+ } else if (ad && ttsText) playTTSForMessage(agentWrap, ttsText);
951
+ }
952
+
953
+ if (!(ttsText && ttsText.trim()) || !state.speakerAuto) {
954
+ showMessage(null);
955
+ return;
956
+ }
957
+ fetch("/tts", {
958
+ method: "POST",
959
+ headers: { "Content-Type": "application/json" },
960
+ body: JSON.stringify({ text: ttsText.trim(), voice_id: state.voiceId || null }),
961
+ })
962
+ .then(function(r) {
963
+ if (!r.ok) throw new Error("TTS failed");
964
+ return r.blob();
965
+ })
966
+ .then(function(blob) { showMessage(blob); })
967
+ .catch(function() { showMessage(null); });
968
+ })
969
+ .catch(function() {
970
+ removeTyping(typingWrap);
971
+ state.history.push({ role: "assistant", content: "Sorry, an error occurred." });
972
+ appendMessage("assistant", "Sorry, an error occurred.", false);
973
+ });
974
+ })
975
+ .catch(function() {});
976
+ };
977
+ mr.start();
978
+ state.recording = true;
979
+ micBtn.classList.add("active");
980
+ micBtn.textContent = "⏹";
981
+ recOverlay.classList.add("visible");
982
+ })
983
+ .catch(function() {});
984
+ });
985
+
986
+ // Theme toggle
987
+ const themeBtn = document.getElementById("theme-btn");
988
+ if (localStorage.getItem("theme") === "light") {
989
+ document.body.classList.add("light-mode");
990
+ themeBtn.textContent = "🌙";
991
+ }
992
+ themeBtn.addEventListener("click", function() {
993
+ const isLight = document.body.classList.toggle("light-mode");
994
+ themeBtn.textContent = isLight ? "🌙" : "☀️";
995
+ localStorage.setItem("theme", isLight ? "light" : "dark");
996
+ });
997
+
998
+ appendMessage("assistant", "Hello, I am the Monaco City Cultural Agent. Ask me about events, exhibitions, or shows in the Principality.", false);
999
+ populateProviders();
1000
+ populateModels();
1001
+ populateVoices();
1002
+ fetch("/last-scraped").then(function(r) { return r.json(); }).then(function(d) {
1003
+ var el = document.getElementById("last-scraped-line");
1004
+ if (el && d && d.last_scraped_at) el.textContent = "Last updated: " + d.last_scraped_at;
1005
+ }).catch(function() {});
1006
+ })();
1007
+ </script>
1008
+ </body>
1009
+ </html>