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
|
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
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()
|
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
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)"]
|
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"
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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"],
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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
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):
|
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"
|
|
||||||
|
|||||||
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/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
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
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
|
* @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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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={
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user