Branch base:
racoon-implObjetivo: nova opção no menu WhatsApp para o cliente enviar uma mídia pronta (áudio ou vídeo), a IA gera apenas título e descrição, e o bot pergunta se deseja publicar numa jornada.
O que é: o cliente já tem o conteúdo pronto (uma aula gravada, um áudio, um vídeo) e quer subir direto para a plataforma — sem geração de conteúdo por IA. A IA entra só para criar título + descrição automaticamente a partir da transcrição.
Diferença para o Podcast: o podcast gera o áudio (AutoContent API). Aqui o cliente envia a mídia já finalizada. Fluxo mais curto e síncrono.
Menu Principal → ✏️ Criar Conteúdo → 📤 Enviar Mídia
→ cliente envia áudio OU vídeo
→ upload Racoon (storage + transcode)
→ transcrição (Whisper) → IA gera título + descrição
→ mostra preview (título/descrição) → confirma
→ "Publicar numa jornada?" → lista jornadas → anexa → URL
| Componente | Arquivo | Uso |
|---|---|---|
| Upload + transcode | app/services/media/racoon_client.py → upload_bytes, wait_for_terminal, extract_playback_url, extract_transcript_text |
Sobe a mídia e obtém URL final |
| Transcrição | app/services/media/openai_transcriber.py → transcribe_media_bytes |
Texto base para a IA |
| Download da mídia do WhatsApp | app/services/media/processor.py → get_media_download_url, _download |
Baixa o arquivo enviado |
| Listar jornadas | app/services/journey/manager.py → list_user_journeys |
Escopo do usuário |
| Anexar à jornada | app/services/journey/manager.py → attach_podcast_to_journey (generalizar p/ vídeo) |
INSERT media+contents+formats+items |
| Seleção de jornada | webhook.py → handlers journey_* / JourneyContext |
Reuso do fluxo de seleção |
| CTA URL / send | app/services/whatsapp/sender.py → send_cta_url, send_message |
Mensagens |
| Contexto de sessão | app/models/schemas.py + session/manager.py |
Novo MediaUploadContext |
schemas.py: novo MediaUploadContextclass MediaUploadContext(BaseModel):
step: str = "awaiting_media" # awaiting_media | confirming | awaiting_journey
media_url: Optional[str] = None
media_kind: Optional[str] = None # "audio" | "video"
racoon_upload_id: Optional[str] = None
racoon_public_id: Optional[str] = None
duration_ms: Optional[int] = None
bytes: Optional[int] = None
gen_title: Optional[str] = None
gen_description: Optional[str] = None
session/manager.py: persistir media_upload_context (get/save) — mesmo padrão dos outros contexts.webhook.py:📤 Enviar Mídia no submenu create_content (ao lado de Quiz e Podcast).upload_media no _static_router: cria MediaUploadContext(step="awaiting_media"), pede o arquivo._handle_message (topo): se media_upload_context ativo e step=awaiting_media e msg é audio/video → _handle_media_upload(msg, ...).webhook.py → _handle_media_upload:get_media_download_url + _download (bytes do WhatsApp)racoon_client.upload_bytes(bytes, name, mime, scope="content")wait_for_terminal(upload_id) (vídeo é assíncrono; áudio idem) → extract_playback_urltranscribe_media_bytes(bytes, ...) OU racoon_client.extract_transcript_text se o Racoon já transcreverapp/services/media/metadata_ai.py:async def generate_title_description(transcript: str, kind: str) -> dict:
# OpenAI JSON: {"title": "...", "description": "..."}
Reaproveita o padrão de quiz/generator.auto_generate_metadata.✅ Confirmar / ✏️ Refazer / ❌ Cancelar.confirm_media_upload: muda step → oferece jornada.list_user_journeys(system_user_id):JourneyContext(content_type="media", ...) + lista (reuso dos botões journey_<id> + Pular).attach_podcast_to_journey → attach_media_to_journey (ou parâmetro item_type):contentType e journey_items.itemType = audio ou video conforme media_kind._handle_journey_selected reconhece content_type == "media" e chama o attach generalizado.tests/test_media_upload.py (mocks de racoon_client/journey):media_url, gen_title, gen_descriptionattach_media_to_journey com item_type corretogenerate_title_description fallback quando transcrição vaziaitemType corretopytest tests/ -v (garantir zero regressão).main.| Arquivo | Função |
|---|---|
app/services/media/metadata_ai.py |
generate_title_description(transcript, kind) — IA p/ título+descrição |
tests/test_media_upload.py |
testes do fluxo |
app/models/schemas.py — MediaUploadContext + campo em SessionStateapp/services/session/manager.py — persistência do novo contextapp/routers/webhook.py — opção de menu + handlers upload_media, _handle_media_upload, confirm_media_upload, branch no _handle_messageapp/services/journey/manager.py — generalizar attach_podcast_to_journey → suportar item_type (audio/video)itemType certo (audio/video)| Risco | Mitigação |
|---|---|
| Vídeo grande demora no transcode | wait_for_terminal com timeout generoso (já usado no podcast); manda "⏳ processando" |
| Transcrição falha/vazia | Fallback: título a partir do nome do arquivo + descrição genérica |
| Usuário sem jornada com permissão | Encerra com "mídia salva" + CTA da URL (sem travar) |
attach_podcast_to_journey muito acoplado a "audio" |
Refatorar com parâmetro item_type, default "audio" (retrocompatível com podcast) |
Aprovar e iniciar pela Hora 1 (menu + contexto). A maior parte é colagem de peças já testadas (Racoon, transcrição, jornada) — risco baixo.