Add new FileSourceType for 'id' and use that for local uploads, prefixed with 'pipecat:'

This commit is contained in:
mattie ruth backman
2026-02-09 11:50:34 -05:00
parent 96e06d2401
commit d9cebe602f
3 changed files with 38 additions and 13 deletions

View File

@@ -250,6 +250,14 @@ class FileUrl(FileSource):
type: FileSourceType = "url" type: FileSourceType = "url"
url: str url: str
public: bool = True
class FileId(FileSource):
"""File source as a file ID."""
type: FileSourceType = "id"
id: str
class File(BaseModel): class File(BaseModel):
@@ -257,7 +265,7 @@ class File(BaseModel):
format: str # Mime format of the file, e.g., 'application/pdf' format: str # Mime format of the file, e.g., 'application/pdf'
name: Optional[str] = None name: Optional[str] = None
source: FileBytes | FileUrl source: FileBytes | FileUrl | FileId
class SendFileOptions(BaseModel): class SendFileOptions(BaseModel):

View File

@@ -8,9 +8,11 @@
import asyncio import asyncio
import base64 import base64
import io
import os import os
from typing import Any, Dict, Mapping, Optional from typing import Any, Dict, Mapping, Optional
import aiohttp
from loguru import logger from loguru import logger
from pydantic import BaseModel, ValidationError from pydantic import BaseModel, ValidationError
@@ -574,19 +576,35 @@ class RTVIProcessor(FrameProcessor):
type = file.source.type type = file.source.type
opts = data.options if data.options is not None else RTVI.SendFileOptions() opts = data.options if data.options is not None else RTVI.SendFileOptions()
if type == "bytes": match type:
source = file.source.bytes case "bytes":
elif type == "url": source = file.source.bytes
if file.source.url.startswith("/files/"): case "url":
if not file.source.public:
# read bytes from URL and encode to base64
type = "bytes"
async with aiohttp.ClientSession() as session:
async with session.get(file.source.url) as response:
content = io.BytesIO(await response.content.read())
base64_string = base64.b64encode(content.getvalue()).decode("utf-8")
source = f"data:{file.format};base64,{base64_string}"
else:
source = file.source.url
case "id":
if not file.source.id.startswith("pipecat:"):
logger.warning(f"Unsupported file ID: {file.source.id}")
self.send_error_response(data.id, f"Unsupported file ID: {file.source.id}")
return
if not self._folder: if not self._folder:
logger.warning( logger.warning(
"Send-file with /files/ URL requires uploads_folder on RTVIProcessor " "Send-file with a pipecat id requires uploads_folder on RTVIProcessor "
"(e.g. uploads_folder=runner_uploads_folder())." "(e.g. uploads_folder=runner_uploads_folder())."
) )
self.send_error_response(data.id, "Uploads folder not set")
return return
# read bytes from file system, encode to base64, then delete the file # read bytes from file system, encode to base64, then delete the file
type = "bytes" type = "bytes"
file_path = os.path.join(self._folder, file.source.url.removeprefix("/files/")) file_path = os.path.join(self._folder, file.source.id.removeprefix("pipecat:"))
with open(file_path, "rb") as f: with open(file_path, "rb") as f:
raw_bytes = f.read() raw_bytes = f.read()
encoded_image = base64.b64encode(raw_bytes).decode("utf-8") encoded_image = base64.b64encode(raw_bytes).decode("utf-8")
@@ -595,11 +613,10 @@ class RTVIProcessor(FrameProcessor):
os.remove(file_path) os.remove(file_path)
except OSError as e: except OSError as e:
logger.warning(f"Failed to remove uploaded file {file_path}: {e}") logger.warning(f"Failed to remove uploaded file {file_path}: {e}")
else: source = file.source.id
source = file.source.url case _:
else: logger.warning(f"Unsupported file source type: {type}")
logger.warning(f"Unsupported file source type: {type}") return
return
if type == "bytes" and file.format.startswith("image/"): if type == "bytes" and file.format.startswith("image/"):
# Only access width/height if the original source is RTVIFileBytes (not RTVIFileUrl) # Only access width/height if the original source is RTVIFileBytes (not RTVIFileUrl)

View File

@@ -311,7 +311,7 @@ def _setup_webrtc_routes(app: FastAPI, args: argparse.Namespace):
# Return the file as a URL that can be used in the client, matching the format of RTVIFile # Return the file as a URL that can be used in the client, matching the format of RTVIFile
return { return {
"name": original_name, "name": original_name,
"source": {"type": "url", "url": f"/files/{safe_name}"}, "source": {"type": "id", "id": f"pipecat:{safe_name}"},
"format": media_type, "format": media_type,
} }