Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions backends/advanced/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ dependencies = [
"redis>=5.0.0",
"rq>=1.16.0",
"soundfile>=0.12.1",
"google-api-python-client>=2.0.0",
"google-auth-oauthlib>=1.0.0",
"google-auth-httplib2>=0.2.0",
"websockets>=12.0",
]

Expand Down
24 changes: 24 additions & 0 deletions backends/advanced/src/advanced_omi_backend/app_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,30 @@ def __init__(self):
# Memory service configuration
self.memory_service_supports_threshold = self.memory_provider == "friend_lite"

self.gdrive_credentials_path = "data/gdrive_service_account.json"
self.gdrive_scopes = ["https://www.googleapis.com/auth/drive.readonly"]
self._gdrive_service = None

def get_gdrive_service(self):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably shouldn't be in app_config. can have a gdrive service in services or have a google_client in clients. Then we initialise it at the point of use

"""Return Google Drive API client using stored service account."""
from google.oauth2.service_account import Credentials
from googleapiclient.discovery import build
if self._gdrive_service:
return self._gdrive_service

if not os.path.exists(self.gdrive_credentials_path):
raise FileNotFoundError(
f"Missing Google Drive credentials at {self.gdrive_credentials_path}"
)

creds = Credentials.from_service_account_file(
self.gdrive_credentials_path,
scopes=self.gdrive_scopes,
)

self._gdrive_service = build("drive", "v3", credentials=creds)
return self._gdrive_service


# Global configuration instance
app_config = AppConfig()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,26 @@
from advanced_omi_backend.auth import current_superuser, current_active_user_optional, get_user_from_token_param
from advanced_omi_backend.controllers import audio_controller
from advanced_omi_backend.models.user import User
from advanced_omi_backend.app_config import get_audio_chunk_dir
from advanced_omi_backend.utils.gdrive_audio_utils import download_audio_files_from_drive

router = APIRouter(prefix="/audio", tags=["audio"])


@router.post("/upload_audio_from_url")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this router name suggests I can use any url, but looks like it's just Gdrive?
Probably the way we want to do this would be to upload from any url, but provide credentials in the header that we get from a google_auth client.

I'm not super familiar with how gdrive works though

async def upload_audio_from_drive_folder(
drive_folder_id: str = Query(...,alias="url", description="Google Drive Folder ID containing audio files (e.g., the string after /folders/ in the URL)"),
current_user: User = Depends(current_superuser),
device_name: str = Query(default="upload"),
auto_generate_client: bool = Query(default=True),
):
files = await download_audio_files_from_drive(drive_folder_id)

return await audio_controller.upload_and_process_audio_files(
current_user, files, device_name, auto_generate_client
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
Comment thread
coderabbitai[bot] marked this conversation as resolved.


@router.get("/get_audio/{conversation_id}")
async def get_conversation_audio(
conversation_id: str,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import os
import io
import tempfile
import atexit
from typing import List
from starlette.datastructures import UploadFile as StarletteUploadFile
from googleapiclient.http import MediaIoBaseDownload
from advanced_omi_backend.app_config import get_app_config

AUDIO_EXTENSIONS = (".wav", ".mp3", ".flac", ".ogg", ".m4a")
FOLDER_MIMETYPE = "application/vnd.google-apps.folder"


class AudioValidationError(Exception):
pass

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

# -------------------------------------------------------------
# DOWNLOAD A SINGLE FILE (OAUTH)
# -------------------------------------------------------------
async def download_and_wrap_drive_file(service, file_item):
file_id = file_item["id"]
name = file_item["name"]

request = service.files().get_media(fileId=file_id)

fh = io.BytesIO()
downloader = MediaIoBaseDownload(fh, request)

done = False
while not done:
status, done = downloader.next_chunk()

content = fh.getvalue()

if not content:
raise AudioValidationError(f"Downloaded Google Drive file '{name}' was empty")

tmp_file = tempfile.SpooledTemporaryFile(max_size=10*1024*1024) # 10 MB
tmp_file.write(content)
tmp_file.seek(0)
upload_file = StarletteUploadFile(filename=name, file=tmp_file)

original_close = upload_file.close

def wrapped_close():
try:
original_close()
finally:
try:
os.unlink(tmp_file.name)
except FileNotFoundError:
pass

upload_file.close = wrapped_close

return upload_file
Comment thread
coderabbitai[bot] marked this conversation as resolved.


# -------------------------------------------------------------
# LIST + DOWNLOAD FILES IN FOLDER (OAUTH)
# -------------------------------------------------------------
async def download_audio_files_from_drive(folder_id: str) -> List[StarletteUploadFile]:
if not folder_id:
raise AudioValidationError("Google Drive folder ID is required.")

service = get_app_config().get_gdrive_service()

try:
query = f"'{folder_id}' in parents and trashed = false"

response = service.files().list(
q=query,
fields="files(id, name, mimeType)",
includeItemsFromAllDrives=False,
supportsAllDrives=False,
).execute()

all_files = response.get("files", [])

audio_files = [
f for f in all_files
if f["name"].lower().endswith(AUDIO_EXTENSIONS)
]

if not audio_files:
raise AudioValidationError("No audio files found in folder.")

wrapped_files = []
for item in audio_files:
wrapped_files.append(await download_and_wrap_drive_file(service, item))

return wrapped_files

except Exception as e:
if isinstance(e, AudioValidationError):
raise
raise AudioValidationError(f"Google Drive API Error: {repr(e)}")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
80 changes: 80 additions & 0 deletions backends/advanced/webui/src/pages/Upload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,53 @@ export default function Upload() {
const [isUploading, setIsUploading] = useState(false)
const [dragActive, setDragActive] = useState(false)
const [uploadProgress, setUploadProgress] = useState(0)
const [audioUrl, setAudioUrl] = useState('')

const { isAdmin } = useAuth()

const generateId = () => Math.random().toString(36).substr(2, 9)

const [urlUploadStatus, setUrlUploadStatus] = useState<{
type: 'success' | 'error' | null
message: string
}>({ type: null, message: '' })


// Handle URL submission
const handleUrlSubmit = async () => {
if (!audioUrl) return

setIsUploading(true)
setUrlUploadStatus({ type: null, message: '' })

try {
const response = await uploadApi.uploadAudioFromUrl({
drive_folder_id: audioUrl,
device_name: 'upload',
auto_generate_client: true,
})

console.log('URL Upload response:', response)

setUrlUploadStatus({
type: 'success',
message: `Audio submitted successfully`,
})

setAudioUrl('')
} catch (err: any) {
console.error('URL upload failed:', err)

setUrlUploadStatus({
type: 'error',
message:
err?.response?.data?.detail || 'Failed to upload audio from URL',
})
} finally {
setIsUploading(false)
}
}

const handleFileSelect = (selectedFiles: FileList | null) => {
if (!selectedFiles) return

Expand Down Expand Up @@ -152,6 +194,44 @@ export default function Upload() {
Upload Audio Files
</h1>
</div>
{/* URL Input */}
<div className="mb-6 p-4 bg-gray-50 dark:bg-gray-700 rounded-lg border border-gray-200 dark:border-gray-600">
<label className="block mb-2 font-medium text-gray-900 dark:text-gray-100">
Paste audio URL:
</label>

<div className="flex space-x-2">
<input
type="text"
value={audioUrl}
onChange={(e) => setAudioUrl(e.target.value)}
placeholder="https://example.com/audio.wav"
className="flex-1 px-3 py-2 border rounded-lg dark:bg-gray-800 dark:text-gray-100"
/>

<button
onClick={handleUrlSubmit}
disabled={isUploading || !audioUrl}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isUploading ? 'Submitting...' : 'Submit URL'}
</button>
</div>

{/* ✅ Add the status message here */}
{urlUploadStatus.type && (
<div
className={`mt-3 p-3 rounded-lg text-sm ${
urlUploadStatus.type === 'success'
? 'bg-green-100 text-green-800 border border-green-300'
: 'bg-red-100 text-red-800 border border-red-300'
}`}
>
{urlUploadStatus.message}
</div>
)}
</div>


{/* Drop Zone */}
<div
Expand Down
17 changes: 15 additions & 2 deletions backends/advanced/webui/src/services/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ export const queueApi = {
}

export const uploadApi = {
uploadAudioFiles: (files: FormData, onProgress?: (progress: number) => void) =>
uploadAudioFiles: (files: FormData, onProgress?: (progress: number) => void) =>
api.post('/api/audio/upload', files, {
headers: { 'Content-Type': 'multipart/form-data' },
timeout: 300000, // 5 minutes
Expand All @@ -188,10 +188,23 @@ export const uploadApi = {
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total)
onProgress(progress)
}
}
},
}),

uploadAudioFromUrl: (payload: { drive_folder_id: string; device_name?: string; auto_generate_client?: boolean }) =>
// 1. Set the POST body to null (or leave it out, though explicit null is cleaner for no body)
api.post('/api/audio/upload_audio_from_url', null, {
// 2. Pass the entire payload object to the 'params' configuration key
params: {
url: payload.drive_folder_id, // IMPORTANT: Use 'url' here to match the backend's alias
device_name: payload.device_name,
auto_generate_client: payload.auto_generate_client,
},
timeout: 300000,
}),
}


export const chatApi = {
// Session management
createSession: (title?: string) => api.post('/api/chat/sessions', { title }),
Expand Down
Loading