mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-20 20:29:06 +00:00
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:
@@ -25,5 +25,6 @@ FROM base AS final
|
||||
COPY --from=builder /venv /venv
|
||||
RUN mkdir -p /app
|
||||
COPY reflector /app/reflector
|
||||
COPY runserver.sh /app/runserver.sh
|
||||
WORKDIR /app
|
||||
CMD ["/venv/bin/python", "-m", "reflector.app"]
|
||||
CMD ["./runserver.sh"]
|
||||
|
||||
110
server/alembic.ini
Normal file
110
server/alembic.ini
Normal 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
|
||||
@@ -129,6 +129,7 @@ class Whisper:
|
||||
translation = result[0].strip()
|
||||
multilingual_transcript[target_language] = translation
|
||||
|
||||
|
||||
return {
|
||||
"text": multilingual_transcript,
|
||||
"words": words
|
||||
@@ -149,7 +150,7 @@ class Whisper:
|
||||
)
|
||||
@asgi_app()
|
||||
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 typing_extensions import Annotated
|
||||
|
||||
@@ -174,9 +175,9 @@ def web():
|
||||
@app.post("/transcribe", dependencies=[Depends(apikey_auth)])
|
||||
async def transcribe(
|
||||
file: UploadFile,
|
||||
timestamp: Annotated[float, Form()] = 0,
|
||||
source_language: Annotated[str, Form()] = "en",
|
||||
target_language: Annotated[str, Form()] = "en"
|
||||
source_language: Annotated[str, Body(...)] = "en",
|
||||
target_language: Annotated[str, Body(...)] = "en",
|
||||
timestamp: Annotated[float, Body()] = 0.0
|
||||
) -> TranscriptResponse:
|
||||
audio_data = await file.read()
|
||||
audio_suffix = file.filename.split(".")[-1]
|
||||
|
||||
1
server/migrations/README
Normal file
1
server/migrations/README
Normal file
@@ -0,0 +1 @@
|
||||
Generic single-database configuration.
|
||||
79
server/migrations/env.py
Normal file
79
server/migrations/env.py
Normal 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()
|
||||
26
server/migrations/script.py.mako
Normal file
26
server/migrations/script.py.mako
Normal 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"}
|
||||
30
server/migrations/versions/543ed284d69a_init.py
Normal file
30
server/migrations/versions/543ed284d69a_init.py
Normal 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 ###
|
||||
@@ -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
99
server/poetry.lock
generated
@@ -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)"]
|
||||
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]]
|
||||
name = "annotated-types"
|
||||
version = "0.5.0"
|
||||
@@ -1726,6 +1745,84 @@ files = [
|
||||
{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]]
|
||||
name = "mpmath"
|
||||
version = "1.3.0"
|
||||
@@ -3298,4 +3395,4 @@ multidict = ">=4.0"
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.11"
|
||||
content-hash = "d84edfea8ac7a849340af8eb5db47df9c13a7cc1c640062ebedb2a808be0de4e"
|
||||
content-hash = "912184d26e5c8916fad9e969697ead9043a8688559063dfca4674ffcd5e2230d"
|
||||
|
||||
@@ -26,6 +26,7 @@ fastapi-pagination = "^0.12.6"
|
||||
databases = {extras = ["aiosqlite", "asyncpg"], version = "^0.7.0"}
|
||||
sqlalchemy = "<1.5"
|
||||
fief-client = {extras = ["fastapi"], version = "^0.17.0"}
|
||||
alembic = "^1.11.3"
|
||||
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import databases
|
||||
import sqlalchemy
|
||||
from reflector.events import subscribers_startup, subscribers_shutdown
|
||||
from reflector.events import subscribers_shutdown, subscribers_startup
|
||||
from reflector.settings import settings
|
||||
|
||||
|
||||
database = databases.Database(settings.DATABASE_URL)
|
||||
metadata = sqlalchemy.MetaData()
|
||||
|
||||
@@ -20,6 +19,8 @@ transcripts = sqlalchemy.Table(
|
||||
sqlalchemy.Column("summary", sqlalchemy.String, nullable=True),
|
||||
sqlalchemy.Column("topics", 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
|
||||
sqlalchemy.Column("user_id", sqlalchemy.String),
|
||||
)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from reflector.processors.base import Processor
|
||||
import importlib
|
||||
|
||||
from reflector.processors.audio_transcript import AudioTranscriptProcessor
|
||||
from reflector.processors.base import Pipeline, Processor
|
||||
from reflector.processors.types import AudioFile
|
||||
from reflector.settings import settings
|
||||
import importlib
|
||||
|
||||
|
||||
class AudioTranscriptAutoProcessor(AudioTranscriptProcessor):
|
||||
@@ -35,6 +36,10 @@ class AudioTranscriptAutoProcessor(AudioTranscriptProcessor):
|
||||
self.processor = self.get_instance(settings.TRANSCRIPT_BACKEND)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def set_pipeline(self, pipeline: Pipeline):
|
||||
super().set_pipeline(pipeline)
|
||||
self.processor.set_pipeline(pipeline)
|
||||
|
||||
def connect(self, processor: Processor):
|
||||
self.processor.connect(processor)
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ API will be a POST request to TRANSCRIPT_URL:
|
||||
from time import monotonic
|
||||
|
||||
import httpx
|
||||
|
||||
from reflector.processors.audio_transcript import AudioTranscriptProcessor
|
||||
from reflector.processors.audio_transcript_auto import AudioTranscriptAutoProcessor
|
||||
from reflector.processors.types import AudioFile, Transcript, TranslationLanguages, Word
|
||||
@@ -54,11 +53,10 @@ class AudioTranscriptModalProcessor(AudioTranscriptProcessor):
|
||||
"file": (data.name, data.fd),
|
||||
}
|
||||
|
||||
# TODO: Get the source / target language from the UI preferences dynamically
|
||||
# Update code here once this is possible.
|
||||
# i.e) extract from context/session objects
|
||||
source_language = "en"
|
||||
target_language = "en"
|
||||
# FIXME this should be a processor after, as each user may want
|
||||
# different languages
|
||||
source_language = self.get_pref("audio:source_language", "en")
|
||||
target_language = self.get_pref("audio:target_language", "en")
|
||||
languages = TranslationLanguages()
|
||||
|
||||
# Only way to set the target should be the UI element like dropdown.
|
||||
@@ -74,7 +72,7 @@ class AudioTranscriptModalProcessor(AudioTranscriptProcessor):
|
||||
files=files,
|
||||
timeout=self.timeout,
|
||||
headers=self.headers,
|
||||
json=json_payload,
|
||||
params=json_payload,
|
||||
)
|
||||
|
||||
self.logger.debug(
|
||||
@@ -84,12 +82,14 @@ class AudioTranscriptModalProcessor(AudioTranscriptProcessor):
|
||||
result = response.json()
|
||||
|
||||
# Sanity check for translation status in the result
|
||||
if target_language in result["text"]:
|
||||
text = result["text"][target_language]
|
||||
else:
|
||||
text = result["text"][source_language]
|
||||
translation = None
|
||||
if source_language != target_language and target_language in result["text"]:
|
||||
translation = result["text"][target_language]
|
||||
text = result["text"][source_language]
|
||||
|
||||
transcript = Transcript(
|
||||
text=text,
|
||||
translation=translation,
|
||||
words=[
|
||||
Word(
|
||||
text=word["text"],
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from reflector.logger import logger
|
||||
from uuid import uuid4
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import asyncio
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from reflector.logger import logger
|
||||
|
||||
|
||||
class Processor:
|
||||
@@ -17,9 +19,11 @@ class Processor:
|
||||
self.uid = uuid4().hex
|
||||
self.flushed = False
|
||||
self.logger = (custom_logger or logger).bind(processor=self.__class__.__name__)
|
||||
self.pipeline = None
|
||||
|
||||
def set_pipeline(self, pipeline: "Pipeline"):
|
||||
# if pipeline is used, pipeline logger will be used instead
|
||||
self.pipeline = pipeline
|
||||
self.logger = pipeline.logger.bind(processor=self.__class__.__name__)
|
||||
|
||||
def connect(self, processor: "Processor"):
|
||||
@@ -54,6 +58,14 @@ class Processor:
|
||||
"""
|
||||
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):
|
||||
for callback in self._callbacks:
|
||||
await callback(data)
|
||||
@@ -191,6 +203,7 @@ class Pipeline(Processor):
|
||||
self.logger.info("Pipeline created")
|
||||
|
||||
self.processors = processors
|
||||
self.prefs = {}
|
||||
|
||||
for processor in processors:
|
||||
processor.set_pipeline(self)
|
||||
@@ -220,3 +233,17 @@ class Pipeline(Processor):
|
||||
for processor in self.processors:
|
||||
processor.describe(level + 1)
|
||||
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)
|
||||
|
||||
@@ -27,7 +27,7 @@ class TranscriptLinerProcessor(Processor):
|
||||
return
|
||||
|
||||
# cut to the next .
|
||||
partial = Transcript(words=[])
|
||||
partial = Transcript(translation=self.transcript.translation, words=[])
|
||||
for word in self.transcript.words[:]:
|
||||
partial.text += word.text
|
||||
partial.words.append(word)
|
||||
@@ -38,7 +38,7 @@ class TranscriptLinerProcessor(Processor):
|
||||
await self.emit(partial)
|
||||
|
||||
# create new transcript
|
||||
partial = Transcript(words=[])
|
||||
partial = Transcript(translation=self.transcript.translation, words=[])
|
||||
|
||||
self.transcript = partial
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ class Word(BaseModel):
|
||||
|
||||
class Transcript(BaseModel):
|
||||
text: str = ""
|
||||
translation: str | None = None
|
||||
words: list[Word] = None
|
||||
|
||||
@property
|
||||
@@ -84,7 +85,7 @@ class Transcript(BaseModel):
|
||||
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):
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
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 json import dumps, loads
|
||||
from pathlib import Path
|
||||
|
||||
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 (
|
||||
Pipeline,
|
||||
AudioChunkerProcessor,
|
||||
AudioFileWriterProcessor,
|
||||
AudioMergeProcessor,
|
||||
AudioTranscriptAutoProcessor,
|
||||
AudioFileWriterProcessor,
|
||||
FinalSummary,
|
||||
Pipeline,
|
||||
TitleSummary,
|
||||
Transcript,
|
||||
TranscriptFinalSummaryProcessor,
|
||||
TranscriptLinerProcessor,
|
||||
TranscriptTopicDetectorProcessor,
|
||||
TranscriptFinalSummaryProcessor,
|
||||
Transcript,
|
||||
TitleSummary,
|
||||
FinalSummary,
|
||||
)
|
||||
|
||||
sessions = []
|
||||
@@ -79,6 +80,8 @@ async def rtc_offer_base(
|
||||
event_callback=None,
|
||||
event_callback_args=None,
|
||||
audio_filename: Path | None = None,
|
||||
source_language: str = "en",
|
||||
target_language: str = "en",
|
||||
):
|
||||
# build an rtc session
|
||||
offer = RTCSessionDescription(sdp=params.sdp, type=params.type)
|
||||
@@ -176,6 +179,8 @@ async def rtc_offer_base(
|
||||
TranscriptFinalSummaryProcessor.as_threaded(callback=on_final_summary),
|
||||
]
|
||||
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
|
||||
# await ctx.pipeline.warmup()
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ class AudioWaveform(BaseModel):
|
||||
|
||||
class TranscriptText(BaseModel):
|
||||
text: str
|
||||
translation: str | None
|
||||
|
||||
|
||||
class TranscriptTopic(BaseModel):
|
||||
@@ -79,6 +80,8 @@ class Transcript(BaseModel):
|
||||
summary: str | None = None
|
||||
topics: list[TranscriptTopic] = []
|
||||
events: list[TranscriptEvent] = []
|
||||
source_language: str = "en"
|
||||
target_language: str = "en"
|
||||
|
||||
def add_event(self, event: str, data: BaseModel) -> TranscriptEvent:
|
||||
ev = TranscriptEvent(event=event, data=data.model_dump())
|
||||
@@ -184,8 +187,19 @@ class TranscriptController:
|
||||
return None
|
||||
return Transcript(**result)
|
||||
|
||||
async def add(self, name: str, user_id: str | None = None):
|
||||
transcript = Transcript(name=name, user_id=user_id)
|
||||
async def add(
|
||||
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())
|
||||
await database.execute(query)
|
||||
return transcript
|
||||
@@ -229,10 +243,14 @@ class GetTranscript(BaseModel):
|
||||
duration: int
|
||||
summary: str | None
|
||||
created_at: datetime
|
||||
source_language: str
|
||||
target_language: str
|
||||
|
||||
|
||||
class CreateTranscript(BaseModel):
|
||||
name: str
|
||||
source_language: str = Field("en")
|
||||
target_language: str = Field("en")
|
||||
|
||||
|
||||
class UpdateTranscript(BaseModel):
|
||||
@@ -241,10 +259,6 @@ class UpdateTranscript(BaseModel):
|
||||
summary: Optional[str] = Field(None)
|
||||
|
||||
|
||||
class TranscriptEntryCreate(BaseModel):
|
||||
name: str
|
||||
|
||||
|
||||
class DeletionStatus(BaseModel):
|
||||
status: str
|
||||
|
||||
@@ -266,7 +280,12 @@ async def transcripts_create(
|
||||
user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)],
|
||||
):
|
||||
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
|
||||
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(
|
||||
transcript,
|
||||
{
|
||||
@@ -568,4 +590,6 @@ async def transcript_record_webrtc(
|
||||
event_callback=handle_rtc_event,
|
||||
event_callback_args=transcript_id,
|
||||
audio_filename=transcript.audio_filename,
|
||||
source_language=transcript.source_language,
|
||||
target_language=transcript.target_language,
|
||||
)
|
||||
|
||||
7
server/runserver.sh
Executable file
7
server/runserver.sh
Executable 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
|
||||
@@ -39,8 +39,20 @@ async def dummy_transcript():
|
||||
|
||||
class TestAudioTranscriptProcessor(AudioTranscriptProcessor):
|
||||
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(
|
||||
text="Hello world",
|
||||
translation=translation,
|
||||
words=[
|
||||
Word(start=0.0, end=1.0, text="Hello"),
|
||||
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
|
||||
ev = events[eventnames.index("TRANSCRIPT")]
|
||||
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
|
||||
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"
|
||||
|
||||
# stop server
|
||||
# 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"
|
||||
server.stop()
|
||||
|
||||
63
server/tests/test_transcripts_translation.py
Normal file
63
server/tests/test_transcripts_translation.py
Normal 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"
|
||||
@@ -1,6 +1,7 @@
|
||||
apis/DefaultApi.ts
|
||||
apis/index.ts
|
||||
index.ts
|
||||
models/AudioWaveform.ts
|
||||
models/CreateTranscript.ts
|
||||
models/DeletionStatus.ts
|
||||
models/GetTranscript.ts
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
import * as runtime from "../runtime";
|
||||
import type {
|
||||
AudioWaveform,
|
||||
CreateTranscript,
|
||||
DeletionStatus,
|
||||
GetTranscript,
|
||||
@@ -23,6 +24,8 @@ import type {
|
||||
UpdateTranscript,
|
||||
} from "../models";
|
||||
import {
|
||||
AudioWaveformFromJSON,
|
||||
AudioWaveformToJSON,
|
||||
CreateTranscriptFromJSON,
|
||||
CreateTranscriptToJSON,
|
||||
DeletionStatusFromJSON,
|
||||
@@ -59,6 +62,10 @@ export interface V1TranscriptGetAudioMp3Request {
|
||||
transcriptId: any;
|
||||
}
|
||||
|
||||
export interface V1TranscriptGetAudioWaveformRequest {
|
||||
transcriptId: any;
|
||||
}
|
||||
|
||||
export interface V1TranscriptGetTopicsRequest {
|
||||
transcriptId: any;
|
||||
}
|
||||
@@ -390,6 +397,67 @@ export class DefaultApi extends runtime.BaseAPI {
|
||||
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
|
||||
*/
|
||||
|
||||
66
www/app/api/models/AudioWaveform.ts
Normal file
66
www/app/api/models/AudioWaveform.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -25,6 +25,18 @@ export interface CreateTranscript {
|
||||
* @memberof CreateTranscript
|
||||
*/
|
||||
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 {
|
||||
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 {
|
||||
name: value.name,
|
||||
source_language: value.sourceLanguage,
|
||||
target_language: value.targetLanguage,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -61,6 +61,18 @@ export interface GetTranscript {
|
||||
* @memberof GetTranscript
|
||||
*/
|
||||
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 && "summary" in value;
|
||||
isInstance = isInstance && "createdAt" in value;
|
||||
isInstance = isInstance && "sourceLanguage" in value;
|
||||
isInstance = isInstance && "targetLanguage" in value;
|
||||
|
||||
return isInstance;
|
||||
}
|
||||
@@ -98,6 +112,8 @@ export function GetTranscriptFromJSONTyped(
|
||||
duration: json["duration"],
|
||||
summary: json["summary"],
|
||||
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,
|
||||
summary: value.summary,
|
||||
created_at: value.createdAt,
|
||||
source_language: value.sourceLanguage,
|
||||
target_language: value.targetLanguage,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export * from "./AudioWaveform";
|
||||
export * from "./CreateTranscript";
|
||||
export * from "./DeletionStatus";
|
||||
export * from "./GetTranscript";
|
||||
|
||||
@@ -15,8 +15,15 @@ body {
|
||||
}
|
||||
|
||||
.temp-transcription {
|
||||
background: beige;
|
||||
background: rgb(151 190 255);
|
||||
border-radius: 5px;
|
||||
border: solid 1px #808080;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.temp-transcription h2 {
|
||||
font-weight: bold;
|
||||
font-size: 130%;
|
||||
}
|
||||
|
||||
.Dropdown-placeholder {
|
||||
@@ -25,3 +32,9 @@ body {
|
||||
.Dropdown-arrow {
|
||||
top: 47% !important;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.audio-source-dropdown .Dropdown-control {
|
||||
max-width: 90px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,14 +56,10 @@ export function Dashboard({
|
||||
|
||||
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">
|
||||
<h1 className="text-2xl font-bold text-blue-500">Meeting Notes</h1>
|
||||
</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
|
||||
visible={!autoscrollEnabled}
|
||||
@@ -84,9 +80,11 @@ export function Dashboard({
|
||||
setActiveTopic(activeTopic?.id == item.id ? null : item)
|
||||
}
|
||||
>
|
||||
<div className="w-1/4">{formatTime(item.timestamp)}</div>
|
||||
<div className="w-3/4 flex justify-between items-center">
|
||||
{item.title}
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="font-light text-slate-500 pr-1">
|
||||
[{formatTime(item.timestamp)}]
|
||||
</span>{" "}
|
||||
<span className="pr-1">{item.title}</span>
|
||||
<FontAwesomeIcon
|
||||
className={`transform transition-transform duration-200`}
|
||||
icon={
|
||||
|
||||
@@ -4,7 +4,7 @@ type FinalSummaryProps = {
|
||||
|
||||
export default function FinalSummary(props: FinalSummaryProps) {
|
||||
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>
|
||||
<p>{props.text}</p>
|
||||
</div>
|
||||
|
||||
@@ -235,7 +235,7 @@ export default function Recorder(props: RecorderProps) {
|
||||
|
||||
return (
|
||||
<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
|
||||
audioDevices={props.audioDevices}
|
||||
setDeviceId={setDeviceId}
|
||||
|
||||
@@ -23,6 +23,7 @@ const useTranscript = (): UseTranscript => {
|
||||
const requestParameters: V1TranscriptsCreateRequest = {
|
||||
createTranscript: {
|
||||
name: "Weekly All-Hands", // Hardcoded for now
|
||||
targetLanguage: "fr", // Hardcoded for now
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -17,6 +17,54 @@ export const useWebSockets = (transcriptId: string | null): UseWebSockets => {
|
||||
const [status, setStatus] = useState<Status>({ value: "disconnected" });
|
||||
|
||||
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;
|
||||
|
||||
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) {
|
||||
case "TRANSCRIPT":
|
||||
if (message.data.text) {
|
||||
setTranscriptText(message.data.text.trim());
|
||||
setTranscriptText(
|
||||
(message.data.translation ?? message.data.text ?? "").trim(),
|
||||
);
|
||||
console.debug("TRANSCRIPT event:", message.data);
|
||||
}
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user