Merge pull request #193 from Monadical-SAS/192-make-old-ui-mobile-friendly-and-replace-english-live-transcription-with-french-translaiton

French translation & UI improvements
This commit is contained in:
Andreas Bonini
2023-08-29 19:57:06 +07:00
committed by GitHub
33 changed files with 960 additions and 75 deletions

View File

@@ -25,5 +25,6 @@ FROM base AS final
COPY --from=builder /venv /venv COPY --from=builder /venv /venv
RUN mkdir -p /app RUN mkdir -p /app
COPY reflector /app/reflector COPY reflector /app/reflector
COPY runserver.sh /app/runserver.sh
WORKDIR /app WORKDIR /app
CMD ["/venv/bin/python", "-m", "reflector.app"] CMD ["./runserver.sh"]

110
server/alembic.ini Normal file
View File

@@ -0,0 +1,110 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = migrations
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
# for all available tokens
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python-dateutil library that can be
# installed by adding `alembic[tz]` to the pip requirements
# string value is passed to dateutil.tz.gettz()
# leave blank for localtime
# timezone =
# max length of characters to apply to the
# "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to migrations/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "version_path_separator" below.
# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions
# version path separator; As mentioned above, this is the character used to split
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
# Valid values for version_path_separator are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
# sqlalchemy.url = driver://user:pass@localhost/dbname
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

View File

@@ -129,6 +129,7 @@ class Whisper:
translation = result[0].strip() translation = result[0].strip()
multilingual_transcript[target_language] = translation multilingual_transcript[target_language] = translation
return { return {
"text": multilingual_transcript, "text": multilingual_transcript,
"words": words "words": words
@@ -149,7 +150,7 @@ class Whisper:
) )
@asgi_app() @asgi_app()
def web(): def web():
from fastapi import Depends, FastAPI, Form, HTTPException, UploadFile, status from fastapi import Body, Depends, FastAPI, HTTPException, UploadFile, status
from fastapi.security import OAuth2PasswordBearer from fastapi.security import OAuth2PasswordBearer
from typing_extensions import Annotated from typing_extensions import Annotated
@@ -174,9 +175,9 @@ def web():
@app.post("/transcribe", dependencies=[Depends(apikey_auth)]) @app.post("/transcribe", dependencies=[Depends(apikey_auth)])
async def transcribe( async def transcribe(
file: UploadFile, file: UploadFile,
timestamp: Annotated[float, Form()] = 0, source_language: Annotated[str, Body(...)] = "en",
source_language: Annotated[str, Form()] = "en", target_language: Annotated[str, Body(...)] = "en",
target_language: Annotated[str, Form()] = "en" timestamp: Annotated[float, Body()] = 0.0
) -> TranscriptResponse: ) -> TranscriptResponse:
audio_data = await file.read() audio_data = await file.read()
audio_suffix = file.filename.split(".")[-1] audio_suffix = file.filename.split(".")[-1]

1
server/migrations/README Normal file
View File

@@ -0,0 +1 @@
Generic single-database configuration.

79
server/migrations/env.py Normal file
View File

@@ -0,0 +1,79 @@
from logging.config import fileConfig
from reflector.settings import settings
from reflector.db import metadata
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
target_metadata = metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
# url = config.get_main_option("sqlalchemy.url")
url = settings.DATABASE_URL
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
alembic_config = config.get_section(config.config_ini_section, {})
alembic_config["sqlalchemy.url"] = settings.DATABASE_URL
connectable = engine_from_config(
alembic_config,
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

View File

@@ -0,0 +1,26 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}

View File

@@ -0,0 +1,30 @@
"""init
Revision ID: 543ed284d69a
Revises:
Create Date: 2023-08-29 10:54:45.142974
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '543ed284d69a'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@@ -0,0 +1,32 @@
"""add source and target language
Revision ID: b3df9681cae9
Revises: 543ed284d69a
Create Date: 2023-08-29 10:55:37.690469
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'b3df9681cae9'
down_revision: Union[str, None] = '543ed284d69a'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('transcript', sa.Column('source_language', sa.String(), nullable=True))
op.add_column('transcript', sa.Column('target_language', sa.String(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('transcript', 'target_language')
op.drop_column('transcript', 'source_language')
# ### end Alembic commands ###

99
server/poetry.lock generated
View File

@@ -289,6 +289,25 @@ files = [
dev = ["aiounittest (==1.4.1)", "attribution (==1.6.2)", "black (==23.3.0)", "coverage[toml] (==7.2.3)", "flake8 (==5.0.4)", "flake8-bugbear (==23.3.12)", "flit (==3.7.1)", "mypy (==1.2.0)", "ufmt (==2.1.0)", "usort (==1.0.6)"] dev = ["aiounittest (==1.4.1)", "attribution (==1.6.2)", "black (==23.3.0)", "coverage[toml] (==7.2.3)", "flake8 (==5.0.4)", "flake8-bugbear (==23.3.12)", "flit (==3.7.1)", "mypy (==1.2.0)", "ufmt (==2.1.0)", "usort (==1.0.6)"]
docs = ["sphinx (==6.1.3)", "sphinx-mdinclude (==0.5.3)"] docs = ["sphinx (==6.1.3)", "sphinx-mdinclude (==0.5.3)"]
[[package]]
name = "alembic"
version = "1.11.3"
description = "A database migration tool for SQLAlchemy."
optional = false
python-versions = ">=3.7"
files = [
{file = "alembic-1.11.3-py3-none-any.whl", hash = "sha256:d6c96c2482740592777c400550a523bc7a9aada4e210cae2e733354ddae6f6f8"},
{file = "alembic-1.11.3.tar.gz", hash = "sha256:3db4ce81a9072e1b5aa44c2d202add24553182672a12daf21608d6f62a8f9cf9"},
]
[package.dependencies]
Mako = "*"
SQLAlchemy = ">=1.3.0"
typing-extensions = ">=4"
[package.extras]
tz = ["python-dateutil"]
[[package]] [[package]]
name = "annotated-types" name = "annotated-types"
version = "0.5.0" version = "0.5.0"
@@ -1726,6 +1745,84 @@ files = [
{file = "makefun-1.15.1.tar.gz", hash = "sha256:40b0f118b6ded0d8d78c78f1eb679b8b6b2462e3c1b3e05fb1b2da8cd46b48a5"}, {file = "makefun-1.15.1.tar.gz", hash = "sha256:40b0f118b6ded0d8d78c78f1eb679b8b6b2462e3c1b3e05fb1b2da8cd46b48a5"},
] ]
[[package]]
name = "mako"
version = "1.2.4"
description = "A super-fast templating language that borrows the best ideas from the existing templating languages."
optional = false
python-versions = ">=3.7"
files = [
{file = "Mako-1.2.4-py3-none-any.whl", hash = "sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818"},
{file = "Mako-1.2.4.tar.gz", hash = "sha256:d60a3903dc3bb01a18ad6a89cdbe2e4eadc69c0bc8ef1e3773ba53d44c3f7a34"},
]
[package.dependencies]
MarkupSafe = ">=0.9.2"
[package.extras]
babel = ["Babel"]
lingua = ["lingua"]
testing = ["pytest"]
[[package]]
name = "markupsafe"
version = "2.1.3"
description = "Safely add untrusted strings to HTML/XML markup."
optional = false
python-versions = ">=3.7"
files = [
{file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"},
{file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"},
{file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"},
{file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"},
{file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"},
{file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"},
{file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"},
{file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"},
{file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"},
{file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"},
{file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"},
{file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"},
{file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"},
{file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"},
{file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"},
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"},
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"},
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
{file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
{file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"},
{file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"},
{file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"},
{file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"},
{file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"},
{file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"},
{file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"},
{file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"},
{file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"},
{file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"},
{file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"},
{file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"},
{file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"},
{file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"},
{file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"},
{file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"},
{file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"},
{file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"},
{file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"},
{file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"},
{file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"},
{file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"},
]
[[package]] [[package]]
name = "mpmath" name = "mpmath"
version = "1.3.0" version = "1.3.0"
@@ -3298,4 +3395,4 @@ multidict = ">=4.0"
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.11" python-versions = "^3.11"
content-hash = "d84edfea8ac7a849340af8eb5db47df9c13a7cc1c640062ebedb2a808be0de4e" content-hash = "912184d26e5c8916fad9e969697ead9043a8688559063dfca4674ffcd5e2230d"

View File

@@ -26,6 +26,7 @@ fastapi-pagination = "^0.12.6"
databases = {extras = ["aiosqlite", "asyncpg"], version = "^0.7.0"} databases = {extras = ["aiosqlite", "asyncpg"], version = "^0.7.0"}
sqlalchemy = "<1.5" sqlalchemy = "<1.5"
fief-client = {extras = ["fastapi"], version = "^0.17.0"} fief-client = {extras = ["fastapi"], version = "^0.17.0"}
alembic = "^1.11.3"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]

View File

@@ -1,9 +1,8 @@
import databases import databases
import sqlalchemy import sqlalchemy
from reflector.events import subscribers_startup, subscribers_shutdown from reflector.events import subscribers_shutdown, subscribers_startup
from reflector.settings import settings from reflector.settings import settings
database = databases.Database(settings.DATABASE_URL) database = databases.Database(settings.DATABASE_URL)
metadata = sqlalchemy.MetaData() metadata = sqlalchemy.MetaData()
@@ -20,6 +19,8 @@ transcripts = sqlalchemy.Table(
sqlalchemy.Column("summary", sqlalchemy.String, nullable=True), sqlalchemy.Column("summary", sqlalchemy.String, nullable=True),
sqlalchemy.Column("topics", sqlalchemy.JSON), sqlalchemy.Column("topics", sqlalchemy.JSON),
sqlalchemy.Column("events", sqlalchemy.JSON), sqlalchemy.Column("events", sqlalchemy.JSON),
sqlalchemy.Column("source_language", sqlalchemy.String, nullable=True),
sqlalchemy.Column("target_language", sqlalchemy.String, nullable=True),
# with user attached, optional # with user attached, optional
sqlalchemy.Column("user_id", sqlalchemy.String), sqlalchemy.Column("user_id", sqlalchemy.String),
) )

View File

@@ -1,8 +1,9 @@
from reflector.processors.base import Processor import importlib
from reflector.processors.audio_transcript import AudioTranscriptProcessor from reflector.processors.audio_transcript import AudioTranscriptProcessor
from reflector.processors.base import Pipeline, Processor
from reflector.processors.types import AudioFile from reflector.processors.types import AudioFile
from reflector.settings import settings from reflector.settings import settings
import importlib
class AudioTranscriptAutoProcessor(AudioTranscriptProcessor): class AudioTranscriptAutoProcessor(AudioTranscriptProcessor):
@@ -35,6 +36,10 @@ class AudioTranscriptAutoProcessor(AudioTranscriptProcessor):
self.processor = self.get_instance(settings.TRANSCRIPT_BACKEND) self.processor = self.get_instance(settings.TRANSCRIPT_BACKEND)
super().__init__(**kwargs) super().__init__(**kwargs)
def set_pipeline(self, pipeline: Pipeline):
super().set_pipeline(pipeline)
self.processor.set_pipeline(pipeline)
def connect(self, processor: Processor): def connect(self, processor: Processor):
self.processor.connect(processor) self.processor.connect(processor)

View File

@@ -15,7 +15,6 @@ API will be a POST request to TRANSCRIPT_URL:
from time import monotonic from time import monotonic
import httpx import httpx
from reflector.processors.audio_transcript import AudioTranscriptProcessor from reflector.processors.audio_transcript import AudioTranscriptProcessor
from reflector.processors.audio_transcript_auto import AudioTranscriptAutoProcessor from reflector.processors.audio_transcript_auto import AudioTranscriptAutoProcessor
from reflector.processors.types import AudioFile, Transcript, TranslationLanguages, Word from reflector.processors.types import AudioFile, Transcript, TranslationLanguages, Word
@@ -54,11 +53,10 @@ class AudioTranscriptModalProcessor(AudioTranscriptProcessor):
"file": (data.name, data.fd), "file": (data.name, data.fd),
} }
# TODO: Get the source / target language from the UI preferences dynamically # FIXME this should be a processor after, as each user may want
# Update code here once this is possible. # different languages
# i.e) extract from context/session objects source_language = self.get_pref("audio:source_language", "en")
source_language = "en" target_language = self.get_pref("audio:target_language", "en")
target_language = "en"
languages = TranslationLanguages() languages = TranslationLanguages()
# Only way to set the target should be the UI element like dropdown. # Only way to set the target should be the UI element like dropdown.
@@ -74,7 +72,7 @@ class AudioTranscriptModalProcessor(AudioTranscriptProcessor):
files=files, files=files,
timeout=self.timeout, timeout=self.timeout,
headers=self.headers, headers=self.headers,
json=json_payload, params=json_payload,
) )
self.logger.debug( self.logger.debug(
@@ -84,12 +82,14 @@ class AudioTranscriptModalProcessor(AudioTranscriptProcessor):
result = response.json() result = response.json()
# Sanity check for translation status in the result # Sanity check for translation status in the result
if target_language in result["text"]: translation = None
text = result["text"][target_language] if source_language != target_language and target_language in result["text"]:
else: translation = result["text"][target_language]
text = result["text"][source_language] text = result["text"][source_language]
transcript = Transcript( transcript = Transcript(
text=text, text=text,
translation=translation,
words=[ words=[
Word( Word(
text=word["text"], text=word["text"],

View File

@@ -1,7 +1,9 @@
from reflector.logger import logger
from uuid import uuid4
from concurrent.futures import ThreadPoolExecutor
import asyncio import asyncio
from concurrent.futures import ThreadPoolExecutor
from typing import Any
from uuid import uuid4
from reflector.logger import logger
class Processor: class Processor:
@@ -17,9 +19,11 @@ class Processor:
self.uid = uuid4().hex self.uid = uuid4().hex
self.flushed = False self.flushed = False
self.logger = (custom_logger or logger).bind(processor=self.__class__.__name__) self.logger = (custom_logger or logger).bind(processor=self.__class__.__name__)
self.pipeline = None
def set_pipeline(self, pipeline: "Pipeline"): def set_pipeline(self, pipeline: "Pipeline"):
# if pipeline is used, pipeline logger will be used instead # if pipeline is used, pipeline logger will be used instead
self.pipeline = pipeline
self.logger = pipeline.logger.bind(processor=self.__class__.__name__) self.logger = pipeline.logger.bind(processor=self.__class__.__name__)
def connect(self, processor: "Processor"): def connect(self, processor: "Processor"):
@@ -54,6 +58,14 @@ class Processor:
""" """
self._callbacks.remove(callback) self._callbacks.remove(callback)
def get_pref(self, key: str, default: Any = None):
"""
Get a preference from the pipeline prefs
"""
if self.pipeline:
return self.pipeline.get_pref(key, default)
return default
async def emit(self, data): async def emit(self, data):
for callback in self._callbacks: for callback in self._callbacks:
await callback(data) await callback(data)
@@ -191,6 +203,7 @@ class Pipeline(Processor):
self.logger.info("Pipeline created") self.logger.info("Pipeline created")
self.processors = processors self.processors = processors
self.prefs = {}
for processor in processors: for processor in processors:
processor.set_pipeline(self) processor.set_pipeline(self)
@@ -220,3 +233,17 @@ class Pipeline(Processor):
for processor in self.processors: for processor in self.processors:
processor.describe(level + 1) processor.describe(level + 1)
logger.info("") logger.info("")
def set_pref(self, key: str, value: Any):
"""
Set a preference for this pipeline
"""
self.prefs[key] = value
def get_pref(self, key: str, default=None):
"""
Get a preference for this pipeline
"""
if key not in self.prefs:
self.logger.warning(f"Pref {key} not found, using default")
return self.prefs.get(key, default)

View File

@@ -27,7 +27,7 @@ class TranscriptLinerProcessor(Processor):
return return
# cut to the next . # cut to the next .
partial = Transcript(words=[]) partial = Transcript(translation=self.transcript.translation, words=[])
for word in self.transcript.words[:]: for word in self.transcript.words[:]:
partial.text += word.text partial.text += word.text
partial.words.append(word) partial.words.append(word)
@@ -38,7 +38,7 @@ class TranscriptLinerProcessor(Processor):
await self.emit(partial) await self.emit(partial)
# create new transcript # create new transcript
partial = Transcript(words=[]) partial = Transcript(translation=self.transcript.translation, words=[])
self.transcript = partial self.transcript = partial

View File

@@ -47,6 +47,7 @@ class Word(BaseModel):
class Transcript(BaseModel): class Transcript(BaseModel):
text: str = "" text: str = ""
translation: str | None = None
words: list[Word] = None words: list[Word] = None
@property @property
@@ -84,7 +85,7 @@ class Transcript(BaseModel):
words = [ words = [
Word(text=word.text, start=word.start, end=word.end) for word in self.words Word(text=word.text, start=word.start, end=word.end) for word in self.words
] ]
return Transcript(text=self.text, words=words) return Transcript(text=self.text, translation=self.translation, words=words)
class TitleSummary(BaseModel): class TitleSummary(BaseModel):

View File

@@ -1,25 +1,26 @@
import asyncio import asyncio
from fastapi import Request, APIRouter
from reflector.events import subscribers_shutdown
from pydantic import BaseModel
from reflector.logger import logger
from aiortc import RTCPeerConnection, RTCSessionDescription, MediaStreamTrack
from json import loads, dumps
from enum import StrEnum from enum import StrEnum
from json import dumps, loads
from pathlib import Path from pathlib import Path
import av import av
from aiortc import MediaStreamTrack, RTCPeerConnection, RTCSessionDescription
from fastapi import APIRouter, Request
from pydantic import BaseModel
from reflector.events import subscribers_shutdown
from reflector.logger import logger
from reflector.processors import ( from reflector.processors import (
Pipeline,
AudioChunkerProcessor, AudioChunkerProcessor,
AudioFileWriterProcessor,
AudioMergeProcessor, AudioMergeProcessor,
AudioTranscriptAutoProcessor, AudioTranscriptAutoProcessor,
AudioFileWriterProcessor, FinalSummary,
Pipeline,
TitleSummary,
Transcript,
TranscriptFinalSummaryProcessor,
TranscriptLinerProcessor, TranscriptLinerProcessor,
TranscriptTopicDetectorProcessor, TranscriptTopicDetectorProcessor,
TranscriptFinalSummaryProcessor,
Transcript,
TitleSummary,
FinalSummary,
) )
sessions = [] sessions = []
@@ -79,6 +80,8 @@ async def rtc_offer_base(
event_callback=None, event_callback=None,
event_callback_args=None, event_callback_args=None,
audio_filename: Path | None = None, audio_filename: Path | None = None,
source_language: str = "en",
target_language: str = "en",
): ):
# build an rtc session # build an rtc session
offer = RTCSessionDescription(sdp=params.sdp, type=params.type) offer = RTCSessionDescription(sdp=params.sdp, type=params.type)
@@ -176,6 +179,8 @@ async def rtc_offer_base(
TranscriptFinalSummaryProcessor.as_threaded(callback=on_final_summary), TranscriptFinalSummaryProcessor.as_threaded(callback=on_final_summary),
] ]
ctx.pipeline = Pipeline(*processors) ctx.pipeline = Pipeline(*processors)
ctx.pipeline.set_pref("audio:source_language", source_language)
ctx.pipeline.set_pref("audio:target_language", target_language)
# FIXME: warmup is not working well yet # FIXME: warmup is not working well yet
# await ctx.pipeline.warmup() # await ctx.pipeline.warmup()

View File

@@ -49,6 +49,7 @@ class AudioWaveform(BaseModel):
class TranscriptText(BaseModel): class TranscriptText(BaseModel):
text: str text: str
translation: str | None
class TranscriptTopic(BaseModel): class TranscriptTopic(BaseModel):
@@ -79,6 +80,8 @@ class Transcript(BaseModel):
summary: str | None = None summary: str | None = None
topics: list[TranscriptTopic] = [] topics: list[TranscriptTopic] = []
events: list[TranscriptEvent] = [] events: list[TranscriptEvent] = []
source_language: str = "en"
target_language: str = "en"
def add_event(self, event: str, data: BaseModel) -> TranscriptEvent: def add_event(self, event: str, data: BaseModel) -> TranscriptEvent:
ev = TranscriptEvent(event=event, data=data.model_dump()) ev = TranscriptEvent(event=event, data=data.model_dump())
@@ -184,8 +187,19 @@ class TranscriptController:
return None return None
return Transcript(**result) return Transcript(**result)
async def add(self, name: str, user_id: str | None = None): async def add(
transcript = Transcript(name=name, user_id=user_id) self,
name: str,
source_language: str = "en",
target_language: str = "en",
user_id: str | None = None,
):
transcript = Transcript(
name=name,
source_language=source_language,
target_language=target_language,
user_id=user_id,
)
query = transcripts.insert().values(**transcript.model_dump()) query = transcripts.insert().values(**transcript.model_dump())
await database.execute(query) await database.execute(query)
return transcript return transcript
@@ -229,10 +243,14 @@ class GetTranscript(BaseModel):
duration: int duration: int
summary: str | None summary: str | None
created_at: datetime created_at: datetime
source_language: str
target_language: str
class CreateTranscript(BaseModel): class CreateTranscript(BaseModel):
name: str name: str
source_language: str = Field("en")
target_language: str = Field("en")
class UpdateTranscript(BaseModel): class UpdateTranscript(BaseModel):
@@ -241,10 +259,6 @@ class UpdateTranscript(BaseModel):
summary: Optional[str] = Field(None) summary: Optional[str] = Field(None)
class TranscriptEntryCreate(BaseModel):
name: str
class DeletionStatus(BaseModel): class DeletionStatus(BaseModel):
status: str status: str
@@ -266,7 +280,12 @@ async def transcripts_create(
user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)], user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)],
): ):
user_id = user["sub"] if user else None user_id = user["sub"] if user else None
return await transcripts_controller.add(info.name, user_id=user_id) return await transcripts_controller.add(
info.name,
source_language=info.source_language,
target_language=info.target_language,
user_id=user_id,
)
# ============================================================== # ==============================================================
@@ -491,7 +510,10 @@ async def handle_rtc_event(event: PipelineEvent, args, data):
# FIXME don't do copy # FIXME don't do copy
if event == PipelineEvent.TRANSCRIPT: if event == PipelineEvent.TRANSCRIPT:
resp = transcript.add_event(event=event, data=TranscriptText(text=data.text)) resp = transcript.add_event(
event=event,
data=TranscriptText(text=data.text, translation=data.translation),
)
await transcripts_controller.update( await transcripts_controller.update(
transcript, transcript,
{ {
@@ -568,4 +590,6 @@ async def transcript_record_webrtc(
event_callback=handle_rtc_event, event_callback=handle_rtc_event,
event_callback_args=transcript_id, event_callback_args=transcript_id,
audio_filename=transcript.audio_filename, audio_filename=transcript.audio_filename,
source_language=transcript.source_language,
target_language=transcript.target_language,
) )

7
server/runserver.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/bash
if [ -f "venv/bin/activate" ]; then
source venv/bin/activate
fi
alembic upgrade head
python -m reflector.app

View File

@@ -39,8 +39,20 @@ async def dummy_transcript():
class TestAudioTranscriptProcessor(AudioTranscriptProcessor): class TestAudioTranscriptProcessor(AudioTranscriptProcessor):
async def _transcript(self, data: AudioFile): async def _transcript(self, data: AudioFile):
source_language = self.get_pref("audio:source_language", "en")
target_language = self.get_pref("audio:target_language", "en")
print("transcripting", source_language, target_language)
print("pipeline", self.pipeline)
print("prefs", self.pipeline.prefs)
translation = None
if source_language != target_language:
if target_language == "fr":
translation = "Bonjour le monde"
return Transcript( return Transcript(
text="Hello world", text="Hello world",
translation=translation,
words=[ words=[
Word(start=0.0, end=1.0, text="Hello"), Word(start=0.0, end=1.0, text="Hello"),
Word(start=1.0, end=2.0, text="world"), Word(start=1.0, end=2.0, text="world"),
@@ -165,6 +177,147 @@ async def test_transcript_rtc_and_websocket(tmpdir, dummy_transcript, dummy_llm)
assert "TRANSCRIPT" in eventnames assert "TRANSCRIPT" in eventnames
ev = events[eventnames.index("TRANSCRIPT")] ev = events[eventnames.index("TRANSCRIPT")]
assert ev["data"]["text"] == "Hello world" assert ev["data"]["text"] == "Hello world"
assert ev["data"]["translation"] is None
assert "TOPIC" in eventnames
ev = events[eventnames.index("TOPIC")]
assert ev["data"]["id"]
assert ev["data"]["summary"] == "LLM SUMMARY"
assert ev["data"]["transcript"].startswith("Hello world")
assert ev["data"]["timestamp"] == 0.0
assert "FINAL_SUMMARY" in eventnames
ev = events[eventnames.index("FINAL_SUMMARY")]
assert ev["data"]["summary"] == "LLM SUMMARY"
# check status order
statuses = [e["data"]["value"] for e in events if e["event"] == "STATUS"]
assert statuses == ["recording", "processing", "ended"]
# ensure the last event received is ended
assert events[-1]["event"] == "STATUS"
assert events[-1]["data"]["value"] == "ended"
# check that transcript status in model is updated
resp = await ac.get(f"/transcripts/{tid}")
assert resp.status_code == 200
assert resp.json()["status"] == "ended"
# check that audio is available
resp = await ac.get(f"/transcripts/{tid}/audio")
assert resp.status_code == 200
assert resp.headers["Content-Type"] == "audio/wav"
# check that audio/mp3 is available
resp = await ac.get(f"/transcripts/{tid}/audio/mp3")
assert resp.status_code == 200
assert resp.headers["Content-Type"] == "audio/mp3"
# stop server
server.stop()
@pytest.mark.asyncio
async def test_transcript_rtc_and_websocket_and_fr(tmpdir, dummy_transcript, dummy_llm):
# goal: start the server, exchange RTC, receive websocket events
# because of that, we need to start the server in a thread
# to be able to connect with aiortc
# with target french language
from reflector.settings import settings
from reflector.app import app
settings.DATA_DIR = Path(tmpdir)
# start server
host = "127.0.0.1"
port = 1255
base_url = f"http://{host}:{port}/v1"
config = Config(app=app, host=host, port=port)
server = ThreadedUvicorn(config)
await server.start()
# create a transcript
ac = AsyncClient(base_url=base_url)
response = await ac.post(
"/transcripts", json={"name": "Test RTC", "target_language": "fr"}
)
assert response.status_code == 200
tid = response.json()["id"]
# create a websocket connection as a task
events = []
async def websocket_task():
print("Test websocket: TASK STARTED")
async with aconnect_ws(f"{base_url}/transcripts/{tid}/events") as ws:
print("Test websocket: CONNECTED")
try:
while True:
msg = await ws.receive_json()
print(f"Test websocket: JSON {msg}")
if msg is None:
break
events.append(msg)
except Exception as e:
print(f"Test websocket: EXCEPTION {e}")
finally:
ws.close()
print("Test websocket: DISCONNECTED")
websocket_task = asyncio.get_event_loop().create_task(websocket_task())
# create stream client
import argparse
from reflector.stream_client import StreamClient
from aiortc.contrib.signaling import add_signaling_arguments, create_signaling
parser = argparse.ArgumentParser()
add_signaling_arguments(parser)
args = parser.parse_args(["-s", "tcp-socket"])
signaling = create_signaling(args)
url = f"{base_url}/transcripts/{tid}/record/webrtc"
path = Path(__file__).parent / "records" / "test_short.wav"
client = StreamClient(signaling, url=url, play_from=path.as_posix())
await client.start()
timeout = 20
while not client.is_ended():
await asyncio.sleep(1)
timeout -= 1
if timeout < 0:
raise TimeoutError("Timeout while waiting for RTC to end")
# XXX aiortc is long to close the connection
# instead of waiting a long time, we just send a STOP
client.channel.send(json.dumps({"cmd": "STOP"}))
# wait the processing to finish
await asyncio.sleep(2)
await client.stop()
# wait the processing to finish
await asyncio.sleep(2)
# stop websocket task
websocket_task.cancel()
# check events
assert len(events) > 0
from pprint import pprint
pprint(events)
# get events list
eventnames = [e["event"] for e in events]
# check events
assert "TRANSCRIPT" in eventnames
ev = events[eventnames.index("TRANSCRIPT")]
assert ev["data"]["text"] == "Hello world"
assert ev["data"]["translation"] == "Bonjour le monde"
assert "TOPIC" in eventnames assert "TOPIC" in eventnames
ev = events[eventnames.index("TOPIC")] ev = events[eventnames.index("TOPIC")]
@@ -186,19 +339,4 @@ async def test_transcript_rtc_and_websocket(tmpdir, dummy_transcript, dummy_llm)
assert events[-1]["data"]["value"] == "ended" assert events[-1]["data"]["value"] == "ended"
# stop server # stop server
# server.stop() server.stop()
# check that transcript status in model is updated
resp = await ac.get(f"/transcripts/{tid}")
assert resp.status_code == 200
assert resp.json()["status"] == "ended"
# check that audio is available
resp = await ac.get(f"/transcripts/{tid}/audio")
assert resp.status_code == 200
assert resp.headers["Content-Type"] == "audio/wav"
# check that audio/mp3 is available
resp = await ac.get(f"/transcripts/{tid}/audio/mp3")
assert resp.status_code == 200
assert resp.headers["Content-Type"] == "audio/mp3"

View File

@@ -0,0 +1,63 @@
import pytest
from httpx import AsyncClient
@pytest.mark.asyncio
async def test_transcript_create_default_translation():
from reflector.app import app
async with AsyncClient(app=app, base_url="http://test/v1") as ac:
response = await ac.post("/transcripts", json={"name": "test en"})
assert response.status_code == 200
assert response.json()["name"] == "test en"
assert response.json()["source_language"] == "en"
assert response.json()["target_language"] == "en"
tid = response.json()["id"]
response = await ac.get(f"/transcripts/{tid}")
assert response.status_code == 200
assert response.json()["name"] == "test en"
assert response.json()["source_language"] == "en"
assert response.json()["target_language"] == "en"
@pytest.mark.asyncio
async def test_transcript_create_en_fr_translation():
from reflector.app import app
async with AsyncClient(app=app, base_url="http://test/v1") as ac:
response = await ac.post(
"/transcripts", json={"name": "test en/fr", "target_language": "fr"}
)
assert response.status_code == 200
assert response.json()["name"] == "test en/fr"
assert response.json()["source_language"] == "en"
assert response.json()["target_language"] == "fr"
tid = response.json()["id"]
response = await ac.get(f"/transcripts/{tid}")
assert response.status_code == 200
assert response.json()["name"] == "test en/fr"
assert response.json()["source_language"] == "en"
assert response.json()["target_language"] == "fr"
@pytest.mark.asyncio
async def test_transcript_create_fr_en_translation():
from reflector.app import app
async with AsyncClient(app=app, base_url="http://test/v1") as ac:
response = await ac.post(
"/transcripts", json={"name": "test fr/en", "source_language": "fr"}
)
assert response.status_code == 200
assert response.json()["name"] == "test fr/en"
assert response.json()["source_language"] == "fr"
assert response.json()["target_language"] == "en"
tid = response.json()["id"]
response = await ac.get(f"/transcripts/{tid}")
assert response.status_code == 200
assert response.json()["name"] == "test fr/en"
assert response.json()["source_language"] == "fr"
assert response.json()["target_language"] == "en"

View File

@@ -1,6 +1,7 @@
apis/DefaultApi.ts apis/DefaultApi.ts
apis/index.ts apis/index.ts
index.ts index.ts
models/AudioWaveform.ts
models/CreateTranscript.ts models/CreateTranscript.ts
models/DeletionStatus.ts models/DeletionStatus.ts
models/GetTranscript.ts models/GetTranscript.ts

View File

@@ -14,6 +14,7 @@
import * as runtime from "../runtime"; import * as runtime from "../runtime";
import type { import type {
AudioWaveform,
CreateTranscript, CreateTranscript,
DeletionStatus, DeletionStatus,
GetTranscript, GetTranscript,
@@ -23,6 +24,8 @@ import type {
UpdateTranscript, UpdateTranscript,
} from "../models"; } from "../models";
import { import {
AudioWaveformFromJSON,
AudioWaveformToJSON,
CreateTranscriptFromJSON, CreateTranscriptFromJSON,
CreateTranscriptToJSON, CreateTranscriptToJSON,
DeletionStatusFromJSON, DeletionStatusFromJSON,
@@ -59,6 +62,10 @@ export interface V1TranscriptGetAudioMp3Request {
transcriptId: any; transcriptId: any;
} }
export interface V1TranscriptGetAudioWaveformRequest {
transcriptId: any;
}
export interface V1TranscriptGetTopicsRequest { export interface V1TranscriptGetTopicsRequest {
transcriptId: any; transcriptId: any;
} }
@@ -390,6 +397,67 @@ export class DefaultApi extends runtime.BaseAPI {
return await response.value(); return await response.value();
} }
/**
* Transcript Get Audio Waveform
*/
async v1TranscriptGetAudioWaveformRaw(
requestParameters: V1TranscriptGetAudioWaveformRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<runtime.ApiResponse<AudioWaveform>> {
if (
requestParameters.transcriptId === null ||
requestParameters.transcriptId === undefined
) {
throw new runtime.RequiredError(
"transcriptId",
"Required parameter requestParameters.transcriptId was null or undefined when calling v1TranscriptGetAudioWaveform.",
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
if (this.configuration && this.configuration.accessToken) {
// oauth required
headerParameters["Authorization"] = await this.configuration.accessToken(
"OAuth2AuthorizationCodeBearer",
[],
);
}
const response = await this.request(
{
path: `/v1/transcripts/{transcript_id}/audio/waveform`.replace(
`{${"transcript_id"}}`,
encodeURIComponent(String(requestParameters.transcriptId)),
),
method: "GET",
headers: headerParameters,
query: queryParameters,
},
initOverrides,
);
return new runtime.JSONApiResponse(response, (jsonValue) =>
AudioWaveformFromJSON(jsonValue),
);
}
/**
* Transcript Get Audio Waveform
*/
async v1TranscriptGetAudioWaveform(
requestParameters: V1TranscriptGetAudioWaveformRequest,
initOverrides?: RequestInit | runtime.InitOverrideFunction,
): Promise<AudioWaveform> {
const response = await this.v1TranscriptGetAudioWaveformRaw(
requestParameters,
initOverrides,
);
return await response.value();
}
/** /**
* Transcript Get Topics * Transcript Get Topics
*/ */

View File

@@ -0,0 +1,66 @@
/* tslint:disable */
/* eslint-disable */
/**
* FastAPI
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
import { exists, mapValues } from "../runtime";
/**
*
* @export
* @interface AudioWaveform
*/
export interface AudioWaveform {
/**
*
* @type {any}
* @memberof AudioWaveform
*/
data: any | null;
}
/**
* Check if a given object implements the AudioWaveform interface.
*/
export function instanceOfAudioWaveform(value: object): boolean {
let isInstance = true;
isInstance = isInstance && "data" in value;
return isInstance;
}
export function AudioWaveformFromJSON(json: any): AudioWaveform {
return AudioWaveformFromJSONTyped(json, false);
}
export function AudioWaveformFromJSONTyped(
json: any,
ignoreDiscriminator: boolean,
): AudioWaveform {
if (json === undefined || json === null) {
return json;
}
return {
data: json["data"],
};
}
export function AudioWaveformToJSON(value?: AudioWaveform | null): any {
if (value === undefined) {
return undefined;
}
if (value === null) {
return null;
}
return {
data: value.data,
};
}

View File

@@ -25,6 +25,18 @@ export interface CreateTranscript {
* @memberof CreateTranscript * @memberof CreateTranscript
*/ */
name: any | null; name: any | null;
/**
*
* @type {any}
* @memberof CreateTranscript
*/
sourceLanguage?: any | null;
/**
*
* @type {any}
* @memberof CreateTranscript
*/
targetLanguage?: any | null;
} }
/** /**
@@ -50,6 +62,12 @@ export function CreateTranscriptFromJSONTyped(
} }
return { return {
name: json["name"], name: json["name"],
sourceLanguage: !exists(json, "source_language")
? undefined
: json["source_language"],
targetLanguage: !exists(json, "target_language")
? undefined
: json["target_language"],
}; };
} }
@@ -62,5 +80,7 @@ export function CreateTranscriptToJSON(value?: CreateTranscript | null): any {
} }
return { return {
name: value.name, name: value.name,
source_language: value.sourceLanguage,
target_language: value.targetLanguage,
}; };
} }

View File

@@ -61,6 +61,18 @@ export interface GetTranscript {
* @memberof GetTranscript * @memberof GetTranscript
*/ */
createdAt: any | null; createdAt: any | null;
/**
*
* @type {any}
* @memberof GetTranscript
*/
sourceLanguage: any | null;
/**
*
* @type {any}
* @memberof GetTranscript
*/
targetLanguage: any | null;
} }
/** /**
@@ -75,6 +87,8 @@ export function instanceOfGetTranscript(value: object): boolean {
isInstance = isInstance && "duration" in value; isInstance = isInstance && "duration" in value;
isInstance = isInstance && "summary" in value; isInstance = isInstance && "summary" in value;
isInstance = isInstance && "createdAt" in value; isInstance = isInstance && "createdAt" in value;
isInstance = isInstance && "sourceLanguage" in value;
isInstance = isInstance && "targetLanguage" in value;
return isInstance; return isInstance;
} }
@@ -98,6 +112,8 @@ export function GetTranscriptFromJSONTyped(
duration: json["duration"], duration: json["duration"],
summary: json["summary"], summary: json["summary"],
createdAt: json["created_at"], createdAt: json["created_at"],
sourceLanguage: json["source_language"],
targetLanguage: json["target_language"],
}; };
} }
@@ -116,5 +132,7 @@ export function GetTranscriptToJSON(value?: GetTranscript | null): any {
duration: value.duration, duration: value.duration,
summary: value.summary, summary: value.summary,
created_at: value.createdAt, created_at: value.createdAt,
source_language: value.sourceLanguage,
target_language: value.targetLanguage,
}; };
} }

View File

@@ -1,5 +1,6 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export * from "./AudioWaveform";
export * from "./CreateTranscript"; export * from "./CreateTranscript";
export * from "./DeletionStatus"; export * from "./DeletionStatus";
export * from "./GetTranscript"; export * from "./GetTranscript";

View File

@@ -15,8 +15,15 @@ body {
} }
.temp-transcription { .temp-transcription {
background: beige; background: rgb(151 190 255);
border-radius: 5px; border-radius: 5px;
border: solid 1px #808080;
margin: 1em 0;
}
.temp-transcription h2 {
font-weight: bold;
font-size: 130%;
} }
.Dropdown-placeholder { .Dropdown-placeholder {
@@ -25,3 +32,9 @@ body {
.Dropdown-arrow { .Dropdown-arrow {
top: 47% !important; top: 47% !important;
} }
@media (max-width: 768px) {
.audio-source-dropdown .Dropdown-control {
max-width: 90px;
}
}

View File

@@ -56,14 +56,10 @@ export function Dashboard({
return ( return (
<> <>
<div className="relative h-[64svh] w-3/4 flex flex-col"> <div className="relative h-[60svh] w-3/4 flex flex-col">
<div className="text-center pb-1 pt-4"> <div className="text-center pb-1 pt-4">
<h1 className="text-2xl font-bold text-blue-500">Meeting Notes</h1> <h1 className="text-2xl font-bold text-blue-500">Meeting Notes</h1>
</div> </div>
<div className="flex justify-between border-b-2">
<div className="w-1/4 font-bold">Timestamp</div>
<div className="w-3/4 font-bold">Topic</div>
</div>
<ScrollToBottom <ScrollToBottom
visible={!autoscrollEnabled} visible={!autoscrollEnabled}
@@ -84,9 +80,11 @@ export function Dashboard({
setActiveTopic(activeTopic?.id == item.id ? null : item) setActiveTopic(activeTopic?.id == item.id ? null : item)
} }
> >
<div className="w-1/4">{formatTime(item.timestamp)}</div> <div className="flex justify-between items-center">
<div className="w-3/4 flex justify-between items-center"> <span className="font-light text-slate-500 pr-1">
{item.title} [{formatTime(item.timestamp)}]
</span>{" "}
<span className="pr-1">{item.title}</span>
<FontAwesomeIcon <FontAwesomeIcon
className={`transform transition-transform duration-200`} className={`transform transition-transform duration-200`}
icon={ icon={

View File

@@ -4,7 +4,7 @@ type FinalSummaryProps = {
export default function FinalSummary(props: FinalSummaryProps) { export default function FinalSummary(props: FinalSummaryProps) {
return ( return (
<div className="min-h-[200px] overflow-y-auto mt-2 p-2 bg-white temp-transcription rounded"> <div className="mt-2 p-2 bg-white temp-transcription rounded">
<h2>Final Summary</h2> <h2>Final Summary</h2>
<p>{props.text}</p> <p>{props.text}</p>
</div> </div>

View File

@@ -235,7 +235,7 @@ export default function Recorder(props: RecorderProps) {
return ( return (
<div className="relative flex flex-col items-center justify-center max-w-[75vw] w-full"> <div className="relative flex flex-col items-center justify-center max-w-[75vw] w-full">
<div className="flex my-2 mx-auto"> <div className="flex my-2 mx-auto audio-source-dropdown">
<AudioInputsDropdown <AudioInputsDropdown
audioDevices={props.audioDevices} audioDevices={props.audioDevices}
setDeviceId={setDeviceId} setDeviceId={setDeviceId}

View File

@@ -23,6 +23,7 @@ const useTranscript = (): UseTranscript => {
const requestParameters: V1TranscriptsCreateRequest = { const requestParameters: V1TranscriptsCreateRequest = {
createTranscript: { createTranscript: {
name: "Weekly All-Hands", // Hardcoded for now name: "Weekly All-Hands", // Hardcoded for now
targetLanguage: "fr", // Hardcoded for now
}, },
}; };

View File

@@ -17,6 +17,54 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => {
const [status, setStatus] = useState<Status>({ value: "disconnected" }); const [status, setStatus] = useState<Status>({ value: "disconnected" });
useEffect(() => { useEffect(() => {
document.onkeyup = (e) => {
if (e.key === "a" && process.env.NEXT_PUBLIC_ENV === "development") {
setTranscriptText("Lorem Ipsum");
setTopics([
{
id: "1",
timestamp: 10,
summary: "This is test topic 1",
title: "Topic 1: Introduction to Quantum Mechanics",
transcript:
"A brief overview of quantum mechanics and its principles.",
},
{
id: "2",
timestamp: 20,
summary: "This is test topic 2",
title: "Topic 2: Machine Learning Algorithms",
transcript:
"Understanding the different types of machine learning algorithms.",
},
{
id: "3",
timestamp: 30,
summary: "This is test topic 3",
title: "Topic 3: Mental Health Awareness",
transcript: "Ways to improve mental health and reduce stigma.",
},
{
id: "4",
timestamp: 40,
summary: "This is test topic 4",
title: "Topic 4: Basics of Productivity",
transcript: "Tips and tricks to increase daily productivity.",
},
{
id: "5",
timestamp: 50,
summary: "This is test topic 5",
title: "Topic 5: Future of Aviation",
transcript:
"Exploring the advancements and possibilities in aviation.",
},
]);
setFinalSummary({ summary: "This is the final summary" });
}
};
if (!transcriptId) return; if (!transcriptId) return;
const url = `${process.env.NEXT_PUBLIC_WEBSOCKET_URL}/v1/transcripts/${transcriptId}/events`; const url = `${process.env.NEXT_PUBLIC_WEBSOCKET_URL}/v1/transcripts/${transcriptId}/events`;
@@ -32,7 +80,9 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => {
switch (message.event) { switch (message.event) {
case "TRANSCRIPT": case "TRANSCRIPT":
if (message.data.text) { if (message.data.text) {
setTranscriptText(message.data.text.trim()); setTranscriptText(
(message.data.translation ?? message.data.text ?? "").trim(),
);
console.debug("TRANSCRIPT event:", message.data); console.debug("TRANSCRIPT event:", message.data);
} }
break; break;