Files
postgenerator/.planning/phases/04-enrichment/04-01-PLAN.md
Michele 6078c75c22 docs(04): create phase plan
Phase 04: Enrichment
- 2 plan(s) in 2 wave(s)
- Wave 1: backend UnsplashService + Settings + pipeline/CSV integration
- Wave 2: frontend Settings UI + PostCard thumbnail + OutputReview hint
- Ready for execution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 01:52:13 +01:00

212 lines
12 KiB
Markdown

---
phase: 04-enrichment
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- backend/services/unsplash_service.py
- backend/schemas/settings.py
- backend/routers/settings.py
- backend/services/csv_builder.py
- backend/services/generation_pipeline.py
- backend/routers/generate.py
autonomous: true
must_haves:
truths:
- "Se unsplash_api_key e' configurata nelle Settings, il CSV contiene URL immagini reali (urls.regular da Unsplash) nelle colonne _image_keyword"
- "Se unsplash_api_key non e' configurata, il CSV contiene le keyword testuali originali senza errori"
- "Keyword identiche nella stessa batch producono lo stesso URL (cache hit)"
- "La cache Unsplash persiste su disco in data/unsplash_cache.json e sopravvive ai riavvii container"
- "Se Unsplash ritorna errore o rate limit, il CSV usa la keyword testuale come fallback senza bloccare l'export"
artifacts:
- path: "backend/services/unsplash_service.py"
provides: "UnsplashService con search, cache disco, fallback"
min_lines: 80
- path: "backend/schemas/settings.py"
provides: "Campo unsplash_api_key nel modello Settings"
contains: "unsplash_api_key"
- path: "backend/services/csv_builder.py"
provides: "Risoluzione keyword -> URL Unsplash nelle colonne _image_keyword"
contains: "resolve"
key_links:
- from: "backend/services/generation_pipeline.py"
to: "backend/services/unsplash_service.py"
via: "Chiamata resolve_keywords dopo generazione LLM"
pattern: "unsplash.*resolve"
- from: "backend/services/csv_builder.py"
to: "image_url_map"
via: "Mappa keyword->URL passata a _build_rows"
pattern: "image_url_map|image_urls"
- from: "backend/routers/settings.py"
to: "unsplash_api_key"
via: "GET/PUT includono il nuovo campo"
pattern: "unsplash"
---
<objective>
Integrare Unsplash API nel backend per risolvere le keyword immagine in URL reali nel CSV.
Purpose: Quando l'utente configura un'API key Unsplash nelle Impostazioni, il CSV esportato contiene URL di immagini reali (~1080px) al posto delle keyword testuali, rendendo il template Canva pronto con immagini di alta qualita'. Se Unsplash non e' configurato o non disponibile, il sistema continua a funzionare con le keyword originali senza interruzioni.
Output: UnsplashService funzionante con cache disco, Settings esteso con unsplash_api_key, pipeline che risolve keyword dopo la generazione LLM, CSVBuilder che scrive URL quando disponibili.
</objective>
<execution_context>
@C:\Users\miche\.claude/get-shit-done/workflows/execute-plan.md
@C:\Users\miche\.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/04-enrichment/04-CONTEXT.md
# File da modificare — leggi PRIMA di implementare
@backend/schemas/settings.py
@backend/routers/settings.py
@backend/services/csv_builder.py
@backend/services/generation_pipeline.py
@backend/routers/generate.py
@backend/config.py
@backend/constants.py
</context>
<tasks>
<task type="auto">
<name>Task 1: UnsplashService + Settings unsplash_api_key</name>
<files>
backend/services/unsplash_service.py
backend/schemas/settings.py
backend/routers/settings.py
</files>
<action>
**1. Crea `backend/services/unsplash_service.py`:**
Classe `UnsplashService` con:
- `__init__(self, api_key: str, cache_path: Path)` — httpx.AsyncClient con base_url `https://api.unsplash.com`, header `Authorization: Client-ID {api_key}`
- `async def search_photo(self, keyword: str) -> Optional[str]` — Cerca 1 foto per keyword, ritorna `urls.regular` (formato ~1080px come da CONTEXT.md). Parametri query: `query={keyword_in_english}`, `per_page=1`, `orientation=landscape`, `content_filter=low`. Se nessun risultato, ritorna None.
- `async def resolve_keywords(self, keywords: list[str]) -> dict[str, str]` — Data una lista di keyword (con possibili duplicati), risolve ognuna in un URL Unsplash. Usa la cache per evitare chiamate duplicate. Ritorna `{keyword: url}` per le keyword risolte. Le keyword non risolvibili NON sono nel dizionario (il caller usa la keyword originale come fallback).
- Cache interna in-memory `self._cache: dict[str, str]` + persistenza su disco in `cache_path` (JSON file, caricato all'init, salvato dopo ogni batch). La cache mappa `keyword -> url`.
- Metodo privato `_translate_keyword(self, keyword: str) -> str` — traduce keyword italiane in inglese per query Unsplash. Approccio pragmatico: usa un dizionario statico di ~30 parole comuni B2B italiane (studio, ufficio, riunione, professionista, dentista, avvocato, imprenditore, cliente, analisi, crescita, successo, team, computer, scrivania, grafici, strategia, contratto, sorriso, stretta di mano, presentazione, azienda, consulenza, marketing, dati, risultati, innovazione, tecnologia, formazione, collaborazione, obiettivo) con relative traduzioni. Per keyword composte, traduce ogni parola individualmente e concatena. Parole non trovate nel dizionario restano invariate (molte keyword di contesto come nomi propri o termini tecnici sono gia' in inglese o comprensibili per Unsplash).
- Retry: 1 tentativo se errore di rete, poi fallback (keyword non risolta). Non ritentare su 401/403 (api key invalida).
- Rate limiting awareness: Legge header `X-Ratelimit-Remaining` dalla response. Se remaining < 5, logga warning e smette di fare richieste per il batch corrente (le keyword restanti tornano non risolte).
- Logging: ogni search logga keyword + risultato (hit cache/miss/errore/rate-limited).
- `async def close(self)` — chiude httpx.AsyncClient.
**2. Aggiungi `unsplash_api_key` a `backend/schemas/settings.py`:**
Aggiungi campo al modello Settings:
```python
unsplash_api_key: Optional[str] = Field(
default=None,
description="Chiave API Unsplash. Se configurata, le keyword immagine vengono risolte in URL reali nel CSV.",
)
```
**3. Aggiorna `backend/routers/settings.py`:**
- Aggiungi `unsplash_api_key_masked: Optional[str]` a `SettingsResponse` — usa la stessa logica di mascheramento di api_key (ultimi 4 caratteri).
- Nel `GET /` ritorna anche `unsplash_api_key_masked=_mask_api_key(settings.unsplash_api_key)`.
- Aggiungi `unsplash_api_key_configured: bool` a `SettingsStatusResponse`.
- Nel `GET /status` ritorna anche `unsplash_api_key_configured=bool(settings.unsplash_api_key)`.
- Nel `PUT /` applica la stessa logica di merge None-preserving: se `new_settings.unsplash_api_key is None`, mantieni quella esistente.
</action>
<verify>
- `python -c "from backend.services.unsplash_service import UnsplashService; print('OK')"` non da errori di import
- `python -c "from backend.schemas.settings import Settings; s = Settings(); print(s.unsplash_api_key)"` stampa `None`
- Verifica che il router settings compili: `python -c "from backend.routers.settings import router; print('OK')"`
</verify>
<done>
UnsplashService creato con search, cache disco, traduzione keyword IT->EN, retry, rate limit awareness. Settings ha campo unsplash_api_key. Router settings espone il nuovo campo mascherato con merge None-preserving.
</done>
</task>
<task type="auto">
<name>Task 2: Integrazione pipeline + CSV con risoluzione Unsplash</name>
<files>
backend/services/generation_pipeline.py
backend/services/csv_builder.py
backend/routers/generate.py
backend/routers/export.py
</files>
<action>
**1. Aggiorna `backend/services/csv_builder.py`:**
Modifica `build_csv()` e `build_csv_content()` per accettare un parametro opzionale `image_url_map: Optional[dict[str, str]] = None`.
In `_build_rows()`, aggiungi parametro `image_url_map: Optional[dict[str, str]] = None`. Nelle righe dove scrive `*_image_keyword`, se `image_url_map` e' presente e la keyword ha un mapping, usa l'URL; altrimenti usa la keyword originale:
```python
def _resolve_image(self, keyword: str, image_url_map: Optional[dict[str, str]]) -> str:
if image_url_map and keyword in image_url_map:
return image_url_map[keyword]
return keyword
```
Applica `_resolve_image()` a: `cover_image_keyword`, ogni `slide.image_keyword`, `cta_image_keyword`.
**2. Aggiorna `backend/services/generation_pipeline.py`:**
Dopo che il loop di generazione ha completato tutti gli slot (dopo il for loop, prima di `self._csv.build_csv()`):
- Carica settings da disco per verificare se `unsplash_api_key` e' configurata
- Se presente, crea UnsplashService con `cache_path=DATA_PATH / "unsplash_cache.json"`
- Estrai tutte le keyword uniche dai PostResult success: `cover_image_keyword`, ogni `slide.image_keyword`, `cta_image_keyword`
- Chiama `await unsplash.resolve_keywords(unique_keywords)` dentro asyncio.to_thread (se necessario) o direttamente se gia' async
- Passa `image_url_map` a `self._csv.build_csv()`
- Chiudi UnsplashService
Salva `image_url_map` nel JobStatus (aggiungere campo opzionale `image_url_map: Optional[dict[str, str]] = None` al dataclass `JobStatus`) per poterlo passare anche all'export con edits.
Includi `image_url_map` nella serializzazione/deserializzazione su disco (`_save_job_to_disk` / `_load_job_from_disk`).
**3. Aggiorna `backend/routers/generate.py`:**
In `_get_pipeline()` e `_get_or_create_pipeline()`, nessuna modifica necessaria — UnsplashService viene creato dentro la pipeline, non iniettato.
**4. Aggiorna `backend/routers/export.py`:**
In `download_csv_with_edits()`, dopo aver caricato il job JSON da disco, recupera `image_url_map` dal JSON (se presente) e passalo a `_csv_builder.build_csv_content()`. Questo garantisce che anche il CSV esportato con edits inline contenga gli URL Unsplash.
**ATTENZIONE**: La risoluzione Unsplash avviene UNA SOLA VOLTA dopo la generazione batch, NON ad ogni download CSV. L'`image_url_map` viene salvato nel job JSON e riutilizzato.
**NOTA**: Per `generate_single()` (rigenerazione singola), NON risolvere Unsplash. La rigenerazione e' veloce e deve restare tale. Gli URL verranno risolti al momento del download CSV dal `image_url_map` del job originale (le keyword nuove che non hanno mapping useranno il fallback keyword).
</action>
<verify>
- `python -c "from backend.services.csv_builder import CSVBuilder; print('OK')"` compila senza errori
- `python -c "from backend.services.generation_pipeline import GenerationPipeline, JobStatus; j = JobStatus(job_id='x', status='running', total=0, completed=0, current_post=0); print(j.image_url_map)"` stampa `None`
- `python -c "from backend.routers.export import router; print('OK')"` compila senza errori
- Verifica che il campo `image_url_map` sia incluso nella serializzazione: `python -c "from backend.services.generation_pipeline import JobStatus; import json; j = JobStatus(job_id='t', status='completed', total=1, completed=1, current_post=0, image_url_map={'test': 'https://example.com/img.jpg'}); print(j.image_url_map)"`
</verify>
<done>
CSVBuilder risolve keyword in URL quando image_url_map e' disponibile. GenerationPipeline risolve keyword via UnsplashService dopo il batch LLM e salva la mappa nel job JSON. Export con edits riutilizza la mappa salvata. generate_single non tocca Unsplash.
</done>
</task>
</tasks>
<verification>
Verifica complessiva backend Phase 4 Plan 01:
1. **Import chain**: `python -c "from backend.main import app; print('FastAPI app OK')"` — nessun errore di import circolare
2. **Settings roundtrip**: Il campo unsplash_api_key viene salvato e caricato correttamente da settings.json
3. **CSV senza Unsplash**: Con unsplash_api_key=None, il CSV contiene le keyword testuali originali (nessuna regressione)
4. **UnsplashService cache**: La cache in-memory evita chiamate duplicate nella stessa sessione
5. **Serializzazione job**: image_url_map viene serializzato in JSON e deserializzato correttamente da _load_job_from_disk
</verification>
<success_criteria>
- UnsplashService creato e importabile senza errori
- Settings ha campo unsplash_api_key con masking nel router
- CSVBuilder accetta image_url_map opzionale e risolve keyword -> URL
- GenerationPipeline integra UnsplashService dopo il batch LLM
- JobStatus include image_url_map con persistenza su disco
- Export con edits riutilizza image_url_map dal job originale
- Nessuna regressione: senza API key Unsplash, tutto funziona come prima
</success_criteria>
<output>
After completion, create `.planning/phases/04-enrichment/04-01-SUMMARY.md`
</output>