Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
30 changes: 22 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,37 @@

This repository contains tools to personalize and expand your Faba+ (and MyFaba) experience.

## Upload Custom .wav Audio Using Faba Me Cloud
## Upload Custom Audio Using Faba Me Cloud

Using the Faba Me sharing functionality, you can upload a .wav file directly to the MyFaba cloud and associate it with your Faba Me robots (white, blue, or red).
Using the Faba Me sharing functionality, you can upload a .wav or .mp3 file directly to the MyFaba cloud and associate it with your Faba Me robots (white, blue, or red).

To upload audio, youll need an Invite to Record URL link. To generate one from the MyFaba mobile app:
To upload audio, you'll need an "Invite to Record" URL link. To generate one from the MyFaba mobile app:
1. Select: _Faba Me_ > _FABA Me White_ (or Red/Blue)
2. Tap _+ Add New Track_ and select _Invite to Record_
3. Copy the last 10 characters of the URL generated (e.g., `8K3TzYl2WB` for `https://studio.myfaba.com/record/8K3TzYl2WB`) as `share_id`
3. Copy the full URL generated (e.g., `https://studio.myfaba.com/it/invites/b92b4a71-19c7-486a-8114-9bdd1e6fe886?token=eyJhbGc...`)

Use the following Python command to upload the file:
Use the following Python command to upload the file, passing the full invite URL:

```bash
python3 myfaba_upload.py [-h] <share_id> <author> <title> <wav_file>
python3 myfaba_upload.py [-h] [-t TOKEN] <invite_url_or_id> <author> <title> <wav_or_mp3_file>
```
e.g.:
```bash
python3 myfaba_upload.py 8K3TzYl2WB "Author Name" "Audio Title" ./audio/test.wav
python3 myfaba_upload.py "https://studio.myfaba.com/it/invites/b92b4a71-19c7-486a-8114-9bdd1e6fe886?token=eyJhbGc..." "Author Name" "Audio Title" ./audio/test.wav
```
Alternatively, pass the bare `invitePublicId` (the UUID in the URL path) together with `-t TOKEN`:
```bash
python3 myfaba_upload.py b92b4a71-19c7-486a-8114-9bdd1e6fe886 "Author Name" "Audio Title" ./audio/test.wav -t eyJhbGc...
```

> [!NOTE]
> The upload may take a few minutes, depending on your internet speed and file size. Once completed, you should receive a notification on the MyFaba mobile app.

> [!IMPORTANT]
> The invite token expires 90 minutes after it's generated, and the API caps uploads at **100MB**. For long recordings, prefer .mp3 over .wav to stay under the limit (a 100kbps stereo mp3 uses roughly 45MB/hour, vs. ~660MB/hour for uncompressed 48kHz .wav).


### Converting .mp3 Files to .wav
### Converting/Concatenating .mp3 Files
To convert .mp3 files to .wav format, you can use these commands:
- Using VLC:
```bash
Expand All @@ -35,6 +43,12 @@ vlc.exe --sout "#transcode{acodec=s16l,channels=2,samplerate=44100}:std{access=f
ffmpeg -i ./audio/test.mp3 -acodec pcm_s16le -ac 2 -ar 44100 ./audio/test.wav
```

To concatenate multiple .mp3 files (e.g., numbered `01.mp3`, `02.mp3`, ...) into a single .mp3 without re-encoding, using FFmpeg:
```bash
for f in *.mp3; do echo "file '$PWD/$f'"; done | sort > list.txt
ffmpeg -f concat -safe 0 -i list.txt -c copy combined.mp3
```

## Manually Adding Audio Files to Faba+
You can manually add .mp3 files to an existing playlist (character) or create a new one using a customized character (new NFC tag). This requires:

Expand Down
258 changes: 108 additions & 150 deletions myfaba_upload.py
Original file line number Diff line number Diff line change
@@ -1,150 +1,108 @@
# Script Name: myfaba_upload.py
# Description: This script allows you to upload custom .wav audio to Faba+ using the Faba Me sharing functionality.
# The share_id is the string of the last 10 characters of the invite to record link.
# To generate the invite link from the mobile app:
# Faba Me > FABA Me White (or Red/Blue), + Add new track > Invite to record
# e.g.: 8K3TzYl2WB for https://studio.myfaba.com/record/8K3TzYl2WB
#
# Note: Uploaded audio will be stored and processed by the MyFaba cloud.
# .mp3 files can be converted to .wav as follow:
# vlc.exe --sout "#transcode{acodec=s16l,channels=2,samplerate=44100}:std{access=file,mux=wav,dst=audio\test.wav}" audio\test.mp3
# ffmpeg -i ./audio/test.mp3 -acodec pcm_s16le -ac 2 -ar 44100 ./audio/test.wav
#
# Usage: python3 myfaba_upload.py [-h] <share_id> <author> <title> <wav_file>
# e.g.: python3 myfaba_upload.py 8K3TzYl2WB "Author Name" "Audio Title" ./audio/test.wav
#
# Author: 60ne https://github.com/60ne/
# Date: 2025-03-16
# Version: 1.0
#
# This script is provided "as is" without warranty of any kind.
#

import re
import wave
import logging
import argparse
import requests
from bs4 import BeautifulSoup
from datetime import datetime
from urllib.parse import urlparse, parse_qs

BASE_URL = "https://studio.myfaba.com/record/"

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')


def check_share_id(share_id):
match = re.search(r"([A-Za-z0-9]{10})$", share_id)
return match.group(1) if match else None


def load_page(session, share_id):
url = f"{BASE_URL}{share_id}"
try:
response = session.get(url, allow_redirects=False)
response.raise_for_status()

if response.status_code == 302:
xsrf_token = session.cookies.get("XSRF-TOKEN")
myfaba_session = session.cookies.get("myfaba_cms_session")
location_url = response.headers.get("Location")

if xsrf_token and myfaba_session and location_url:
logging.info("Loading page")
return xsrf_token, myfaba_session, location_url

logging.error(f"Unexpected response status: {response.status_code}")
except requests.RequestException as e:
logging.error(f"Failed to fetch parameters: {e}")
return None, None, None


def fetch_parameters(session, xsrf_token, myfaba_session, location_url):
headers = {"Cookie": f"XSRF-TOKEN={xsrf_token}; myfaba_cms_session={myfaba_session}"}
try:
response = session.get(location_url, headers=headers)
response.raise_for_status()

soup = BeautifulSoup(response.text, "html.parser")
form = soup.find("form", {"id": "form"})

if form:
action_url = form["action"]
query_params = parse_qs(urlparse(action_url).query)
_token = soup.find("input", {"name": "_token"})

if _token and action_url:
logging.info("Parameters extracted successfully")
return action_url, query_params.get("expires", [None])[0], query_params.get("signature", [None])[0], _token["value"]

logging.error("Form or token not found")
except requests.RequestException as e:
logging.error(f"Failed to fetch form parameters: {e}")
return None, None, None, None


def get_wav_duration(wav_path):
try:
with wave.open(wav_path, "rb") as wav_file:
return int(wav_file.getnframes() / float(wav_file.getframerate()))
except (wave.Error, FileNotFoundError) as e:
logging.error(f"Error reading WAV file: {e}")
return None


def upload_wav(session, action_url, xsrf_token, myfaba_session, _token, wav_path, author, title):
duration = get_wav_duration(wav_path)
if duration is None:
logging.error("Invalid .wav file duration. Check .wav file")
return False

headers = {"Cookie": f"XSRF-TOKEN={xsrf_token}; myfaba_cms_session={myfaba_session}"}
data = {"_token": _token, "duration": str(duration), "creator": author, "title": title}

try:
with open(wav_path, "rb") as audio_file:
files = {"userAudio": ("recorded.wav", audio_file, "audio/wav")}
response = session.post(action_url, headers=headers, files=files, data=data)
response.raise_for_status()
logging.info("Upload successfully completed!")
logging.info("Check Faba mobile app")
return True
except (requests.RequestException, IOError) as e:
logging.error(f"Upload failed: {e}")
return False


def main():
parser = argparse.ArgumentParser(description="Upload custom .wav audio to Faba+ using the Faba Me sharing functionality")
parser.add_argument("share_id", help="The share_id is the string of the last 10 characters of the invite to record link")
parser.add_argument("author", help="Author name")
parser.add_argument("title", help="Audio title")
parser.add_argument("wav_path", help="Path of .wav file to upload")
args = parser.parse_args()

share_id = check_share_id(args.share_id)
if not share_id:
logging.error("Invalid share_id format")
exit(1)

session = requests.Session()
xsrf_token, myfaba_session, location_url = load_page(session, args.share_id)

if xsrf_token and myfaba_session and location_url:
parsed_url = urlparse(location_url)
query_params = parse_qs(parsed_url.query)
expires_timestamp = int(query_params.get('expires', [0])[0])
expires_datetime = datetime.utcfromtimestamp(expires_timestamp)
logging.info("share_id valid until: %s", expires_datetime.strftime('%Y-%m-%d %H:%M:%S UTC'))

action_url, expires, signature, _token = fetch_parameters(session, xsrf_token, myfaba_session, location_url)

if action_url and _token:
success = upload_wav(session, action_url, xsrf_token, myfaba_session, _token, args.wav_path, args.author, args.title)
if not success:
logging.error("Upload failed, try again")


if __name__ == "__main__":
main()
# Script Name: myfaba_upload.py
# Description: This script allows you to upload custom .wav audio to Faba+ using the Faba Me
# "Invite to Record" sharing functionality.
#
# MyFaba Studio invite links now look like:
# https://studio.myfaba.com/<lang>/invites/<invitePublicId>?token=<jwt>
# e.g.: https://studio.myfaba.com/it/invites/b92b4a71-19c7-486a-8114-9bdd1e6fe886?token=eyJ...
#
# Pass either the full URL or just the <invitePublicId> and <token> separately.
#
# Note: Uploaded audio will be stored and processed by the MyFaba cloud.
# .mp3 files can be converted to .wav as follow:
# vlc.exe --sout "#transcode{acodec=s16l,channels=2,samplerate=44100}:std{access=file,mux=wav,dst=audio\test.wav}" audio\test.mp3
# ffmpeg -i ./audio/test.mp3 -acodec pcm_s16le -ac 2 -ar 44100 ./audio/test.wav
#
# Usage: python3 myfaba_upload.py [-h] <invite_url_or_id> <author> <title> <wav_file> [-t TOKEN]
# e.g.: python3 myfaba_upload.py "https://studio.myfaba.com/it/invites/<id>?token=<jwt>" "Author Name" "Audio Title" ./audio/test.wav
#
# Author: 60ne https://github.com/60ne/
# Date: 2025-03-16
# Version: 2.0
#
# This script is provided "as is" without warranty of any kind.
#

import re
import logging
import argparse
import requests
from urllib.parse import urlparse, parse_qs

API_URL = "https://api.myfaba.com/api/v3/studio/invites/{invite_id}/recordings"

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

UUID_RE = re.compile(r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}")


def parse_invite(invite_arg, token_arg):
"""Accepts either a full invite URL (with ?token=...) or a bare invitePublicId + separate token."""
if invite_arg.startswith("http"):
parsed = urlparse(invite_arg)
match = UUID_RE.search(parsed.path)
if not match:
return None, None
invite_id = match.group(0)
token = parse_qs(parsed.query).get("token", [None])[0]
return invite_id, token

invite_id = invite_arg if UUID_RE.fullmatch(invite_arg) else None
return invite_id, token_arg


def upload_wav(invite_id, token, wav_path, author, title):
url = API_URL.format(invite_id=invite_id)
params = {"token": token}
ext = wav_path.rsplit(".", 1)[-1].lower()
mime = {"wav": "audio/wav", "mp3": "audio/mpeg"}.get(ext, "application/octet-stream")

try:
with open(wav_path, "rb") as audio_file:
files = {"audio": (f"recording.{ext}", audio_file, mime)}
data = {"title": title}
if author:
data["creator"] = author

response = requests.post(url, params=params, files=files, data=data)
except (requests.RequestException, IOError) as e:
logging.error(f"Upload failed: {e}")
return False

try:
payload = response.json()
except ValueError:
logging.error(f"Upload failed: unexpected response ({response.status_code}): {response.text[:300]}")
return False

if response.status_code >= 200 and response.status_code < 300 and payload.get("success"):
logging.info("Upload successfully completed!")
logging.info("Check Faba mobile app")
return True

logging.error(f"Upload failed: {payload.get('error')} - {payload.get('message')}")
return False


def main():
parser = argparse.ArgumentParser(description="Upload custom .wav audio to Faba+ using the Faba Me sharing functionality")
parser.add_argument("invite", help="Full invite URL (https://studio.myfaba.com/<lang>/invites/<id>?token=<jwt>) or bare invitePublicId")
parser.add_argument("author", help="Author name")
parser.add_argument("title", help="Audio title")
parser.add_argument("wav_path", help="Path of .wav file to upload")
parser.add_argument("-t", "--token", help="JWT token, required if 'invite' is a bare invitePublicId rather than a full URL")
args = parser.parse_args()

invite_id, token = parse_invite(args.invite, args.token)
if not invite_id or not token:
logging.error("Could not determine invitePublicId/token. Pass the full invite URL, or invitePublicId with -t TOKEN.")
exit(1)

logging.info(f"Uploading to invite {invite_id}")
success = upload_wav(invite_id, token, args.wav_path, args.author, args.title)
if not success:
exit(1)


if __name__ == "__main__":
main()