Pular para o conteúdo principal

Integre o reSpeaker Clip ao Seu Serviço

Este guia mostra como adicionar o reSpeaker Clip como uma fonte de áudio gerenciada a um serviço Python existente. Ele se concentra na fronteira de integração: um adaptador de dispositivo, uma conexão de longa duração, uma pequena superfície de API e uma propriedade de estado bem definida.

O reSpeaker Clip é uma fonte de entrada. ClipClient lida com a comunicação com o dispositivo, ClipService o adapta à sua aplicação, e o seu serviço mantém a responsabilidade pelo processamento e pela lógica de negócio.

Para configuração de transporte, a API completa do ClipClient, comandos AT e o protocolo de transferência de arquivos, consulte o Guia Básico do SDK do reSpeaker Clip. Esta página não volta a explicar BLE/GATT, formatos de quadro, tratamento de CRC ou ferramentas de CLI.

Abordagem de Integração

Mantenha o dispositivo atrás de um adaptador estreito e envie gravações baixadas para o ponto de entrada de processamento que o seu serviço já utiliza:

Existing service with reSpeaker Clip as a new audio source

  1. Crie um ClipService para cada dispositivo físico.
  2. Conecte-o durante a inicialização da aplicação e desconecte-o durante o encerramento.
  3. Exponha apenas as operações de gravação de que o seu serviço precisa.
  4. Passe arquivos de áudio concluídos para o pipeline de processamento existente.

Antes de Começar

Este guia pressupõe que você já consegue se comunicar com um Clip na sua própria máquina. Antes de continuar, certifique-se de que você consegue:

  • instalar o SDK Python do reSpeaker Clip e conectar a um Clip;
  • iniciar e parar uma gravação;
  • listar sessões concluídas;
  • baixar uma gravação com sucesso.

Se qualquer um destes itens falhar, pare aqui e conclua primeiro o Guia Básico do SDK do reSpeaker Clip. O Guia Básico do SDK é a fonte de verdade para configuração de transporte, comandos do dispositivo e detalhes de transferência de arquivos; este guia faz referência a ele em vez de repeti-lo.

Escolha o Guia Certo do reSpeaker Clip

A documentação do reSpeaker Clip é organizada por camada de desenvolvimento. Comece com Getting Started para configuração do produto e fluxos de trabalho normais. Use o Basic SDK quando um aplicativo host precisar controlar o dispositivo ou baixar gravações. Continue para Service Integration quando um serviço de longa duração precisar ser o responsável pela conexão com o dispositivo e expor APIs. Use o Firmware SDK somente quando você precisar alterar o comportamento no lado do dispositivo, protocolos ou processamento de áudio.

Adicione o reSpeaker Clip como uma Fonte de Áudio ao Seu Serviço Existente

Siga as etapas abaixo para conectar o adaptador de dispositivo a um serviço FastAPI existente.

Etapa 1: Preparar o Pipeline

Pressuponha que o seu serviço já tenha uma função que aceita um arquivo de áudio:

async def process_audio(path: Path) -> dict:
# your existing processing pipeline
...

Uma rota de upload existente já chama essa função:

@app.post("/audio")
async def upload_audio(file: UploadFile):
path = await save_upload(file)
return await process_audio(path)

A integração do Clip deve gerar um arquivo de áudio local padrão e chamar a mesma função process_audio(). O processamento deve continuar independente do dispositivo de captura.

Encapsule essa função no objeto de serviço usado pela rota de ingestão do Clip:

# audio_service.py
from pathlib import Path


class AudioService:
def __init__(self, recordings_dir: Path):
self.recordings_dir = recordings_dir
self.recordings_dir.mkdir(parents=True, exist_ok=True)

async def process(self, path: Path) -> dict:
return await process_audio(path)

Etapa 2: Criar o adaptador

Encapsule apenas as operações necessárias para esta integração: conectar, desconectar, iniciar, parar e baixar.

# clip_service.py
from __future__ import annotations
from pathlib import Path

from clip import ClipClient, ClipError


class ClipUnavailable(Exception):
"""The Clip device is not connected or could not perform an operation."""


class ClipStateError(Exception):
"""The device is in the wrong state for this operation."""


class ClipService:
"""Application-level adapter over a single long-lived ClipClient."""


self._client = ClipClient(transport, command_timeout=command_timeout)
self._connected = False
self._active_session: str | None = None

async def connect(self) -> None:
await self._client.connect()
self._connected = True


if self._connected:
await self._client.disconnect()
self._connected = False

async def start_recording(self, mode: str | None = None) -> str:
if not self._connected:
raise ClipUnavailable("Clip is not connected")
if self._active_session is not None:
raise ClipStateError("a recording is already in progress")
try:
session_id = await self._client.start_recording(mode)
except ClipError as exc:
raise ClipUnavailable("Clip could not start recording") from exc

raise ClipStateError("Clip did not return a session id")
self._active_session = session_id
return session_id

async def stop_recording(self) -> str | None:
if not self._connected:
raise ClipUnavailable("Clip is not connected")
if self._active_session is None:
raise ClipStateError("no recording in progress")
session_id = self._active_session
try:
await self._client.stop_recording()

raise ClipUnavailable("Clip could not stop recording") from exc
self._active_session = None
return session_id

async def download_session(self, session_id: str, destination: str | Path):
if not self._connected:
raise ClipUnavailable("Clip is not connected")
try:
return await self._client.download_session(session_id, destination)
except ClipError as exc:
raise ClipUnavailable("Clip could not download the recording") from exc

Etapa 3: Vincular ciclo de vida

Crie o adaptador durante a inicialização do FastAPI, armazene-o no estado da aplicação e desconecte-o durante o desligamento.

Service lifecycle for reSpeaker Clip integration

Vincule a conexão ao ciclo de vida do FastAPI para que ela seja aberta uma vez na inicialização e fechada uma vez na parada:

# main.py
from contextlib import asynccontextmanager
from pathlib import Path

from fastapi import FastAPI

from clip import BleTransport # or UdpTransport after the Wi-Fi AP is on
from clip_service import ClipService
from audio_service import AudioService


@asynccontextmanager
async def lifespan(app: FastAPI):
transport = BleTransport(name="Clip")
clip = ClipService(transport)
await clip.connect()
app.state.clip = clip
app.state.audio = AudioService(recordings_dir=Path("recordings"))
try:
yield
finally:
await clip.disconnect()


app = FastAPI(lifespan=lifespan)

Etapa 4: Adicionar rotas

Crie endpoints para iniciar, parar e ingerir uma gravação concluída:

POST /clip/recordings/start
POST /clip/recordings/stop
POST /clip/sessions/{session_id}/ingest
# routes.py
from pathlib import Path

from fastapi import APIRouter, HTTPException, Request

from audio_service import AudioService
from clip_service import ClipService, ClipStateError, ClipUnavailable

router = APIRouter(prefix="/clip")


@router.post("/recordings/start")
async def start_recording(request: Request, mode: str | None = None):
clip: ClipService = request.app.state.clip
try:
session_id = await clip.start_recording(mode)
except ClipStateError as exc:
raise HTTPException(status_code=409, detail=str(exc))
except ClipUnavailable as exc:
raise HTTPException(status_code=503, detail=str(exc))
return {"session_id": session_id}


@router.post("/recordings/stop")
async def stop_recording(request: Request):
clip: ClipService = request.app.state.clip
try:
session_id = await clip.stop_recording()
except ClipStateError as exc:
raise HTTPException(status_code=409, detail=str(exc))
except ClipUnavailable as exc:
raise HTTPException(status_code=503, detail=str(exc))
return {"session_id": session_id, "status": "stopped"}


@router.post("/sessions/{session_id}/ingest")
async def ingest_session(session_id: str, request: Request):
clip: ClipService = request.app.state.clip
audio: AudioService = request.app.state.audio
try:
result = await clip.download_session(session_id, audio.recordings_dir)
except ClipUnavailable as exc:
raise HTTPException(status_code=502, detail=str(exc))
ingested = [await audio.process(Path(file.path)) for file in result.files]
return {"session_id": session_id, "files": ingested}

Etapa 5: Registrar o roteador

Importe o roteador em main.py e registre-o após criar a aplicação FastAPI:

from routes import router

app = FastAPI(lifespan=lifespan)
app.include_router(router)

A integração agora consiste em main.py, routes.py, clip_service.py e audio_service.py no pacote da sua aplicação.

Etapa 6: Verificar a API

Inicie o serviço e, em seguida, chame as rotas na seguinte ordem:

curl -X POST "http://localhost:8000/clip/recordings/start"
curl -X POST "http://localhost:8000/clip/recordings/stop"
curl -X POST "http://localhost:8000/clip/sessions/<session_id>/ingest"

As respostas de início e parada devem conter o mesmo session_id. A resposta de ingestão deve conter os arquivos retornados pelo pipeline AudioService existente.

Considerações de produção para um serviço de longa duração

Mantenha o estado de conexão, erro e fluxo de trabalho explícito quando a integração estiver em execução como um serviço.

Limite do adaptador

Mantenha todas as chamadas de ClipClient dentro de ClipService. O adaptador é responsável pelo acesso à conexão, comandos de gravação, downloads e tradução de erros do SDK; ele não é responsável por processamento, armazenamento ou outra lógica de negócio.

O código acima do adaptador deve usar conceitos da aplicação, como session_id e exceções da aplicação, em vez de detalhes de transporte, comando AT ou transferência de arquivos.

Reutilização do cliente

Mapeie um Clip físico para um ClipClient de longa duração e um ClipService. Como o firmware não possui ID de requisição, ClipClient serializa comandos. Para vários dispositivos, indexe instâncias de ClipService pelo ID do dispositivo em um ClipManager.

Tempo de vida da conexão

Um erro comum é abrir o dispositivo dentro do manipulador de requisição:

# WRONG — do not do this in a service
@app.post("/clip/start")
async def start():
async with ClipClient(BleTransport(name="Clip")) as clip:
await clip.start_recording()

Isso reconecta a cada chamada e perde a sessão ativa entre as requisições. O ciclo de vida da aplicação é o dono da conexão; as requisições a reutilizam.

Limite de erro

Traduza exceções do SDK em exceções da aplicação dentro de ClipService e, em seguida, mapeie-as para respostas HTTP no limite da rota.

Um mapeamento de exemplo simples (ilustrativo, não uma exigência do SDK):

Condição da aplicaçãoStatus HTTP
Clip indisponível503
Estado inválido do dispositivo409
Falha no comando do dispositivo502
Requisição inválida do cliente400

Os códigos exatos são específicos da aplicação. O código fora do adaptador deve depender de ClipUnavailable e ClipStateError, não de exceções do SDK.

Reconexão

Em caso de erro do dispositivo, marque o dispositivo como indisponível e reconecte ou recrie o transporte. Um tempo limite de comando exige reconexão antes do próximo comando, porque o transporte não consegue mais correlacionar respostas. Exponha a falha como ClipUnavailable e, em seguida, reconecte a partir de uma tarefa em segundo plano ou da próxima requisição.

Idempotência

Repetições de HTTP, repetições de workers e reinicializações podem enviar o mesmo session_id mais de uma vez. Verifique o estado persistente antes de processá-lo.

Acompanhe cada sessão no seu banco de dados:

session_iddownloadedprocessedresult_idcreated_at
Sessão única do dispositivoStatus de downloadStatus do pipelineReferência do resultadoHorário da primeira ingestão
async def ingest_clip_session(session_id: str):
if repository.is_processed(session_id):
return repository.get_result(session_id)

result = await clip_service.download_session(session_id, Path("recordings"))
for file in result.files:
await process_audio(Path(file.path))

repository.mark_processed(session_id)
return repository.get_result(session_id)

Isso evita processamento duplicado e efeitos colaterais a jusante.

Propriedade de estado

Mantenha o estado do dispositivo separado do estado do fluxo de trabalho:

EstadoResponsávelValores de exemplo
Estado do ClipCamada de integração do dispositivo (ClipService)conectado, gravando, sessão disponível, disponível para download
Estado do fluxo de trabalhoCamada de aplicação/jobingerindo, processando, concluído, com falha

Não una as duas máquinas de estado em um único campo. Uma sessão pode ser baixada enquanto o fluxo de trabalho da aplicação ainda está em execução.

Próximas etapas

Depois que o adaptador estiver funcionando, mantenha a integração do dispositivo separada de qualquer processamento específico da aplicação. Páginas relacionadas:

Suporte técnico e discussão sobre o produto

Obrigado por escolher nossos produtos! Estamos aqui para oferecer diferentes tipos de suporte para garantir que sua experiência com nossos produtos seja a mais tranquila possível. Oferecemos vários canais de comunicação para atender a diferentes preferências e necessidades.

Loading Comments...