Initial release: DictIA v0.8.14-alpha (fork de Speakr, AGPL-3.0)
This commit is contained in:
63
.dockerignore
Normal file
63
.dockerignore
Normal file
@@ -0,0 +1,63 @@
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Docker
|
||||
.dockerignore
|
||||
Dockerfile
|
||||
docker-compose*.yml
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
venv/
|
||||
.venv/
|
||||
|
||||
# Data directories (mounted as volumes)
|
||||
data/
|
||||
uploads/
|
||||
instance/
|
||||
exports/
|
||||
auto-process/
|
||||
|
||||
# Non-runtime directories
|
||||
temp/
|
||||
docs/
|
||||
site/
|
||||
plan/
|
||||
unraid/
|
||||
tests/
|
||||
.claude/
|
||||
.migrate/
|
||||
.github/
|
||||
|
||||
# IDE and editor files
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Logs and misc
|
||||
*.log
|
||||
*.md
|
||||
!requirements*.txt
|
||||
LICENSE
|
||||
CLAUDE.md
|
||||
29
.gitattributes
vendored
Normal file
29
.gitattributes
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
# Ensure shell scripts always use LF line endings, even on Windows
|
||||
*.sh text eol=lf
|
||||
|
||||
# Ensure Python files use LF line endings
|
||||
*.py text eol=lf
|
||||
|
||||
# Docker files should use LF
|
||||
Dockerfile text eol=lf
|
||||
docker-compose*.yml text eol=lf
|
||||
.dockerignore text eol=lf
|
||||
|
||||
# Config files should use LF
|
||||
*.example text eol=lf
|
||||
*.conf text eol=lf
|
||||
*.config text eol=lf
|
||||
|
||||
# Documentation uses LF
|
||||
*.md text eol=lf
|
||||
|
||||
# Binary files
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.ico binary
|
||||
*.gif binary
|
||||
*.webp binary
|
||||
*.db binary
|
||||
*.pyc binary
|
||||
|
||||
60
.gitignore
vendored
Normal file
60
.gitignore
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
.playwright-mcp/
|
||||
venv/
|
||||
__pycache__/
|
||||
instance/
|
||||
uploads/
|
||||
*.db
|
||||
*.db-journal
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.log
|
||||
*.env
|
||||
.migrate/
|
||||
project_files.md
|
||||
*.md
|
||||
notes.md
|
||||
docker-compose.yml
|
||||
changes.txt
|
||||
|
||||
!README.md
|
||||
!CLA.md
|
||||
!CONTRIBUTING.md
|
||||
!CHANGES.md
|
||||
!UPSTREAM-SYNC.md
|
||||
!.github/CLA-SETUP.md
|
||||
!docs/**/*.md
|
||||
!deployment/**/*.md
|
||||
!client_docs/**/*.md
|
||||
!client_docs/*.md
|
||||
docker-compose.dev.yml
|
||||
docker-compose.lite.yml
|
||||
docker-compose.postgres.yml
|
||||
.clinerules
|
||||
temp/
|
||||
.claude/
|
||||
plan/
|
||||
|
||||
# Offline vendor dependencies (downloaded during Docker build)
|
||||
static/vendor/
|
||||
|
||||
# Docs build artifacts
|
||||
docs/_site/
|
||||
docs/.jekyll-cache/
|
||||
docs/README.md
|
||||
site/
|
||||
.cache/
|
||||
docs/overrides/.cache/
|
||||
|
||||
# Documentation deployment files (examples only)
|
||||
docs/.github-deploy.yml
|
||||
|
||||
# Documentation conversion scripts (one-time use)
|
||||
scripts/convert_to_mkdocs.py
|
||||
unraid/
|
||||
|
||||
# Internal ops files — do not commit to public repo
|
||||
CLAUDE.md
|
||||
UPSTREAM-SYNC.md
|
||||
sync-upstream.sh
|
||||
scripts/pre-sync-check.sh
|
||||
scripts/check-dictia.sh
|
||||
1
.upstream-version
Normal file
1
.upstream-version
Normal file
@@ -0,0 +1 @@
|
||||
v0.8.14-alpha
|
||||
121
CHANGES.md
Normal file
121
CHANGES.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# CHANGES — DictIA (fork de Speakr)
|
||||
|
||||
Ce fichier documente les modifications apportees par InnovA AI au projet
|
||||
[Speakr](https://github.com/murtaza-nasir/speakr) original, conformement a
|
||||
la licence AGPL-3.0 (section 5).
|
||||
|
||||
---
|
||||
|
||||
## [dictia-fr-prompts] — 2026-03-09
|
||||
|
||||
### Traduction FR et amelioration des prompts LLM
|
||||
|
||||
**Prompts traduits et optimises** :
|
||||
- Prompt de resume fallback : traduit en FR avec sections claires (Sujets, Decisions, Actions)
|
||||
- Message systeme de resume : role prompting en FR ("expert en redaction de comptes rendus")
|
||||
- Prompt d'identification des locuteurs : traduit en FR avec exemples francophones
|
||||
- Prompt d'extraction d'evenements : traduit en FR avec exemples negatifs/positifs adaptes
|
||||
- Message systeme d'extraction d'evenements : traduit en FR
|
||||
- Labels de contexte (date, dossier, etiquettes, info utilisateur) : tous traduits en FR
|
||||
- Directives linguistiques traduites en FR
|
||||
|
||||
**Templates traduits (fallback text)** :
|
||||
- `account.html` : section Nommage complete, Hotwords, Invite initiale, langues, poste, entreprise
|
||||
- `account.html` : liste des langues de transcription (Anglais, Francais, etc.)
|
||||
- `account.html` : onglets Transcription/Exportation/Nommage
|
||||
- `admin.html` : tags d'equipe (langue, intervenants, retention, partage auto, protection)
|
||||
- `fr.json` : correction "Created" → "Cree"
|
||||
|
||||
---
|
||||
|
||||
## [v0.8.13-sync] — 2026-02-26
|
||||
|
||||
### Sync upstream Speakr v0.8.9-alpha → v0.8.13-alpha
|
||||
|
||||
Mise a jour majeure incluant 4 releases upstream :
|
||||
|
||||
**Nouvelles features (depuis upstream)** :
|
||||
- Video retention (`VIDEO_RETENTION=true`) — garde les videos pour lecture in-browser
|
||||
- Uploads paralleles (`MAX_CONCURRENT_UPLOADS=3`)
|
||||
- Detection de doublons SHA-256
|
||||
- Speaker API avec authentification bearer token
|
||||
- Volume controls sur tous les players audio/video
|
||||
- Speaker search/filter sur la page management
|
||||
- Auto-scroll follow-along sur les pages partagees
|
||||
- Fix ffprobe timeout dynamique pour gros fichiers video
|
||||
|
||||
**Bugfixes upstream inclus** :
|
||||
- Fix enregistrements incognito pas affiches apres transcription
|
||||
- Fix parametre langue pour connecteurs sans diarisation
|
||||
- Fix defaults PostgreSQL double-quotes
|
||||
- Fix MAX_CONTENT_LENGTH pas mis a jour
|
||||
- Fix copyright dynamique sur pages partagees
|
||||
|
||||
### Reorganisation du repo
|
||||
|
||||
- **Branche unique `dictia`** : remplace les 6 anciennes branches (dictia-branding, dictia-deployment, feature/defaults-fr, feature/loi25-audit-trail, feat/logo-dictia, fix/logo-size)
|
||||
- **Miroir upstream** : nouveau repo `Innova-AI/speakr-upstream` sur Gitea, sync auto toutes les 8h
|
||||
- **Process simplifie** : mise a jour upstream = 3 commandes (fetch + merge + build)
|
||||
- Anciennes branches supprimees, `dictia` est la branche par defaut
|
||||
- Documentation mise a jour (CLAUDE.md, UPSTREAM-SYNC.md)
|
||||
|
||||
---
|
||||
|
||||
## [dictia-branding] — 2026-02-11
|
||||
|
||||
### Rebranding visuel Speakr → DictIA
|
||||
|
||||
Toutes les occurrences visuelles modifiees :
|
||||
|
||||
**Titres de pages** : index, login, register, account, admin, group-admin, inquire, auth/*
|
||||
**Headers et logos** : header principal, login, register, account, admin, group-admin, inquire, share, auth/*
|
||||
**PWA** : manifest.json, sw.js, offline.html, loading_overlay.html
|
||||
**Traductions** : footer d export dans les 6 fichiers locales
|
||||
**Logo** : logo-dictia.png (micro + ondes + reseau IA)
|
||||
**Traductions FR** : page login complete, modal aide audio systeme
|
||||
|
||||
### Footer legal (Loi 25 du Quebec + AGPL-3.0)
|
||||
|
||||
Footer sur toutes les pages contenant :
|
||||
- Lien vers le code source (obligation AGPL-3.0)
|
||||
- Lien Politique de confidentialite (Loi 25)
|
||||
- Lien Conditions d utilisation (Loi 25)
|
||||
|
||||
---
|
||||
|
||||
## [dictia-audit] — 2026-02-11
|
||||
|
||||
### Audit trail Loi 25
|
||||
|
||||
- `src/models/access_log.py` : modele AccessLog
|
||||
- `src/models/auth_log.py` : modele AuthLog
|
||||
- `src/api/audit.py` : endpoints API audit
|
||||
- Integration dans `src/app.py` (ENABLE_AUDIT_LOG)
|
||||
|
||||
---
|
||||
|
||||
## [dictia-defaults] — 2026-02-11
|
||||
|
||||
### Defaults FR
|
||||
|
||||
- `src/init_db.py` : dossiers actives, prompt FR structure, limites augmentees
|
||||
- `enable_folders=true`, `max_file_size_mb=10000`, `transcript_length_limit=50000`
|
||||
|
||||
---
|
||||
|
||||
## [dictia-deployment] — 2026-02-11
|
||||
|
||||
### Infrastructure de deploiement
|
||||
|
||||
- Docker Compose : cloud, local-gpu, local-cpu
|
||||
- ASR Proxy GCP GPU (fallback multi-zone Canada)
|
||||
- Securite : iptables, Docker log rotation
|
||||
- Nginx reverse proxy + Tailscale HTTPS
|
||||
- Scripts : backup, restore, update, health-check
|
||||
- Documentation : quickstart, VPS setup, maintenance, troubleshooting
|
||||
|
||||
---
|
||||
|
||||
Projet original : Speakr par Murtaza Nasir — https://github.com/murtaza-nasir/speakr
|
||||
Copyright original : (c) Speakr contributors, sous licence AGPL-3.0
|
||||
Modifications : (c) 2026 InnovA AI — https://innova-ai.ca
|
||||
82
Dockerfile
Normal file
82
Dockerfile
Normal file
@@ -0,0 +1,82 @@
|
||||
###############################################################################
|
||||
# Stage 1: Builder — install Python deps and download vendor assets
|
||||
###############################################################################
|
||||
FROM python:3.11-slim AS builder
|
||||
|
||||
ARG PRODUCTION=0
|
||||
ARG LIGHTWEIGHT=1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# gcc is needed to compile C extensions during pip install
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends gcc \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Python dependencies
|
||||
COPY requirements.txt requirements-embeddings.txt constraints.txt ./
|
||||
RUN pip install --no-cache-dir --prefix=/install -c constraints.txt -r requirements.txt && \
|
||||
if [ "$LIGHTWEIGHT" = "0" ]; then \
|
||||
pip install --no-cache-dir --prefix=/install -c constraints.txt -r requirements-embeddings.txt; \
|
||||
fi
|
||||
|
||||
# Download vendor assets (JS/CSS/fonts)
|
||||
RUN mkdir -p /app/static/vendor
|
||||
COPY scripts/download_offline_deps.py scripts/
|
||||
RUN pip install --no-cache-dir requests && \
|
||||
PRODUCTION=${PRODUCTION} python scripts/download_offline_deps.py && \
|
||||
echo "✓ Vendor dependencies downloaded successfully"
|
||||
|
||||
###############################################################################
|
||||
# Stage 2: FFmpeg — download static binaries (much smaller than apt ffmpeg)
|
||||
###############################################################################
|
||||
FROM python:3.11-slim AS ffmpeg-stage
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends wget xz-utils \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& ARCH=$(dpkg --print-architecture) \
|
||||
&& wget -q https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-${ARCH}-static.tar.xz -O /tmp/ff.tar.xz \
|
||||
&& mkdir -p /tmp/ffmpeg-dir \
|
||||
&& tar xf /tmp/ff.tar.xz -C /tmp/ffmpeg-dir --strip-components=1 \
|
||||
&& mv /tmp/ffmpeg-dir/ffmpeg /usr/local/bin/ffmpeg \
|
||||
&& mv /tmp/ffmpeg-dir/ffprobe /usr/local/bin/ffprobe \
|
||||
&& chmod +x /usr/local/bin/ffmpeg /usr/local/bin/ffprobe \
|
||||
&& rm -rf /tmp/ff.tar.xz /tmp/ffmpeg-dir
|
||||
|
||||
###############################################################################
|
||||
# Stage 3: Runtime — lean final image with only what's needed
|
||||
###############################################################################
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy static ffmpeg binaries (~150MB vs ~450MB from apt)
|
||||
COPY --from=ffmpeg-stage /usr/local/bin/ffmpeg /usr/local/bin/ffmpeg
|
||||
COPY --from=ffmpeg-stage /usr/local/bin/ffprobe /usr/local/bin/ffprobe
|
||||
|
||||
# Copy installed Python packages from builder
|
||||
COPY --from=builder /install /usr/local
|
||||
|
||||
# Copy downloaded vendor assets from builder
|
||||
COPY --from=builder /app/static/vendor /app/static/vendor
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
# Create necessary directories
|
||||
RUN mkdir -p /data/uploads /data/instance && chmod 755 /data/uploads /data/instance
|
||||
|
||||
# Set environment variables
|
||||
ENV FLASK_APP=src/app.py
|
||||
ENV SQLALCHEMY_DATABASE_URI=sqlite:////data/instance/transcriptions.db
|
||||
ENV UPLOAD_FOLDER=/data/uploads
|
||||
ENV PYTHONPATH=/app
|
||||
ENV HF_HOME=/data/instance/huggingface
|
||||
|
||||
# Add entrypoint script
|
||||
COPY scripts/docker-entrypoint.sh /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||
|
||||
EXPOSE 8899
|
||||
|
||||
ENTRYPOINT ["docker-entrypoint.sh"]
|
||||
CMD ["gunicorn", "--workers", "3", "--bind", "0.0.0.0:8899", "--timeout", "600", "src.app:app"]
|
||||
661
LICENSE
Normal file
661
LICENSE
Normal file
@@ -0,0 +1,661 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
5
NOTICE
Normal file
5
NOTICE
Normal file
@@ -0,0 +1,5 @@
|
||||
DictIA — Transcription Audio par IA
|
||||
Copyright (C) 2026 InnovA AI
|
||||
|
||||
AGPL-3.0 — voir LICENSE
|
||||
Oeuvre originale: github.com/murtaza-nasir/speakr (C) 2024-2026 Murtaza Nasir
|
||||
35
README.md
Normal file
35
README.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# DictIA — Transcription Audio par IA
|
||||
|
||||
Application de transcription audio propulsee par l'intelligence artificielle. Transformez vos enregistrements en texte structure avec identification des locuteurs, resumes automatiques et recherche semantique.
|
||||
|
||||
## Fonctionnalites
|
||||
|
||||
- Transcription automatique avec diarisation (identification des locuteurs)
|
||||
- Resumes et points d'action generes par IA
|
||||
- Recherche semantique dans vos transcriptions (mode Inquire)
|
||||
- Interface web moderne (PWA installable)
|
||||
- Support multilingue (francais, anglais, allemand, espagnol, russe, chinois)
|
||||
- Gestion des dossiers, tags et partage
|
||||
- Conformite Loi 25 (Quebec) — journal d'audit integre
|
||||
- 100% auto-heberge — vos donnees restent chez vous
|
||||
|
||||
## Demarrage rapide
|
||||
|
||||
Voir le [guide de demarrage](client_docs/guide-utilisateur/premiers-pas.md).
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Guide utilisateur](client_docs/guide-utilisateur/index.md)
|
||||
- [Guide administrateur](client_docs/guide-admin/index.md)
|
||||
- [Depannage](client_docs/depannage/index.md)
|
||||
|
||||
## Deploiement
|
||||
|
||||
Voir le [guide de deploiement](deployment/README.md) et les profils Docker dans `deployment/profiles/`.
|
||||
|
||||
## Licence
|
||||
|
||||
AGPL-3.0 — voir [LICENSE](LICENSE).
|
||||
Base sur le projet open source [Speakr](https://github.com/murtaza-nasir/speakr) par Murtaza Nasir.
|
||||
|
||||
Copyright (C) 2026 InnovA AI
|
||||
175
client_docs/depannage/fonctionnalites.md
Normal file
175
client_docs/depannage/fonctionnalites.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# Dépannage : Fonctionnalités
|
||||
|
||||
Ce guide couvre les problèmes liés aux fonctionnalités spécifiques de DictIA et les solutions pour les résoudre.
|
||||
|
||||
---
|
||||
|
||||
## Identification des locuteurs
|
||||
|
||||
### Les locuteurs ne sont pas identifiés
|
||||
|
||||
Si vos transcriptions n'affichent aucune distinction entre les locuteurs (tout le texte est attribué à un seul intervenant), cela peut être dû à la configuration du service de transcription.
|
||||
|
||||
**Que faire** :
|
||||
|
||||
- Vérifiez que l'identification des locuteurs est bien activée dans vos paramètres. Lors du téléversement ou du retraitement d'un enregistrement, assurez-vous que l'option d'identification des locuteurs est cochée.
|
||||
- Vérifiez les paramètres de nombre de locuteurs. Si le nombre minimum et maximum sont tous les deux réglés à 1, l'identification est effectivement désactivée. Utilisez une plage raisonnable, par exemple 2 à 6 locuteurs pour la plupart des réunions.
|
||||
- Si le problème persiste, contactez le support InnovA AI.
|
||||
|
||||
### Tous les locuteurs affichent « LOCUTEUR INCONNU »
|
||||
|
||||
Si la transcription affiche « LOCUTEUR INCONNU » au lieu de locuteurs numérotés (LOCUTEUR_00, LOCUTEUR_01, etc.), cela indique un problème de configuration du service de transcription côté serveur.
|
||||
|
||||
**Que faire** :
|
||||
|
||||
- Contactez le support InnovA AI pour vérifier la configuration du service de transcription.
|
||||
|
||||
### Comment identifier les locuteurs après la transcription
|
||||
|
||||
Après la transcription, les locuteurs apparaissent avec des étiquettes génériques (LOCUTEUR_01, LOCUTEUR_02, etc.). Pour les identifier :
|
||||
|
||||
1. Cliquez sur l'étiquette d'un locuteur dans la transcription.
|
||||
2. Attribuez-lui un nom (par exemple, « Marie Tremblay »).
|
||||
3. Ce profil de locuteur est sauvegardé dans votre bibliothèque personnelle.
|
||||
4. Lors de futurs enregistrements, DictIA peut suggérer automatiquement l'identité des locuteurs en fonction de leurs profils vocaux.
|
||||
|
||||
Gérez votre bibliothèque de locuteurs dans *Paramètres du compte* > *Gestion des locuteurs*.
|
||||
|
||||
---
|
||||
|
||||
## Partage d'enregistrements
|
||||
|
||||
### Le bouton de partage est désactivé
|
||||
|
||||
Le partage par lien externe nécessite que votre instance DictIA soit accessible via une connexion sécurisée (HTTPS). Si le bouton de partage est désactivé ou affiche une erreur, cela signifie que cette condition n'est pas remplie.
|
||||
|
||||
**Que faire** :
|
||||
|
||||
- Contactez le support InnovA AI pour configurer l'accès HTTPS sur votre instance.
|
||||
|
||||
### Les liens de partage ne fonctionnent pas
|
||||
|
||||
Si vous avez créé un lien de partage mais que les destinataires ne peuvent pas y accéder :
|
||||
|
||||
**Que faire** :
|
||||
|
||||
- Vérifiez que le lien n'a pas été révoqué. Consultez vos partages actifs dans l'interface de gestion des partages.
|
||||
- Assurez-vous que le destinataire dispose d'une connexion internet et utilise un navigateur récent.
|
||||
- Si le problème persiste, contactez le support InnovA AI.
|
||||
|
||||
### Partage interne (entre utilisateurs)
|
||||
|
||||
Pour partager un enregistrement avec un collègue qui dispose d'un compte DictIA :
|
||||
|
||||
- Utilisez la fonction de partage interne plutôt que les liens externes.
|
||||
- Recherchez l'utilisateur par son nom d'utilisateur et définissez le niveau d'accès souhaité : lecture seule, modification ou re-partage.
|
||||
- Le destinataire verra l'enregistrement partagé dans sa liste.
|
||||
|
||||
---
|
||||
|
||||
## Recherche IA (recherche sémantique)
|
||||
|
||||
### La Recherche IA ne retourne aucun résultat
|
||||
|
||||
La Recherche IA utilise une technologie de recherche sémantique qui comprend le sens et le contexte de vos questions, plutôt que de chercher des mots-clés exacts.
|
||||
|
||||
**Causes possibles** :
|
||||
|
||||
- **Enregistrements non indexés** : Vos enregistrements doivent être traités et indexés avant d'être consultables via la Recherche IA. Ce processus peut prendre un certain temps après la transcription.
|
||||
- **Formulation de la question** : La Recherche IA fonctionne mieux avec des questions complètes plutôt qu'avec des mots isolés.
|
||||
|
||||
**Que faire** :
|
||||
|
||||
- Posez des questions complètes. Par exemple, « Qu'avons-nous décidé concernant le budget? » fonctionne mieux que simplement « budget décision ».
|
||||
- Patientez quelques minutes après une nouvelle transcription pour que l'indexation soit complétée.
|
||||
- Si aucun enregistrement n'apparaît jamais dans les résultats de recherche, contactez le support InnovA AI pour vérifier que la recherche sémantique est activée et fonctionnelle.
|
||||
|
||||
### Résultats de recherche non pertinents
|
||||
|
||||
**Que faire** :
|
||||
|
||||
- Reformulez votre question de manière plus précise ou plus détaillée.
|
||||
- Essayez différentes formulations pour le même sujet.
|
||||
- Gardez en tête que la Recherche IA comprend le sens, pas les mots exacts. « Combien coûte le projet? » trouvera du contenu qui parle de « budget » ou de « montant estimé ».
|
||||
|
||||
---
|
||||
|
||||
## Application mobile
|
||||
|
||||
### Installation de l'application mobile
|
||||
|
||||
DictIA est une application web progressive (PWA). Cela signifie que vous n'avez pas besoin de l'installer depuis un magasin d'applications. Pour l'installer sur votre appareil :
|
||||
|
||||
**Sur iPhone/iPad (Safari)** :
|
||||
|
||||
1. Ouvrez DictIA dans Safari.
|
||||
2. Appuyez sur le bouton de partage (icône avec une flèche vers le haut).
|
||||
3. Sélectionnez « Sur l'écran d'accueil ».
|
||||
|
||||
**Sur Android (Chrome)** :
|
||||
|
||||
1. Ouvrez DictIA dans Chrome.
|
||||
2. Appuyez sur le menu (trois points) en haut à droite.
|
||||
3. Sélectionnez « Installer l'application » ou « Ajouter à l'écran d'accueil ».
|
||||
|
||||
### L'application mobile ne fonctionne pas correctement
|
||||
|
||||
**Que faire** :
|
||||
|
||||
- Assurez-vous d'utiliser un navigateur compatible (Safari sur iOS, Chrome sur Android).
|
||||
- Vérifiez votre connexion internet.
|
||||
- Essayez de supprimer l'application de votre écran d'accueil et de la réinstaller.
|
||||
- Videz le cache du navigateur si l'application se comporte de manière inattendue.
|
||||
|
||||
### L'enregistrement audio ne fonctionne pas sur mobile
|
||||
|
||||
Certains navigateurs mobiles ont des restrictions sur l'accès au microphone.
|
||||
|
||||
**Que faire** :
|
||||
|
||||
- Assurez-vous que DictIA a la permission d'accéder au microphone de votre appareil.
|
||||
- Sur iOS, utilisez Safari (les autres navigateurs peuvent avoir des limitations).
|
||||
- Vérifiez que votre navigateur est à jour.
|
||||
|
||||
---
|
||||
|
||||
## Connexion et accès au compte
|
||||
|
||||
### Impossible de se connecter
|
||||
|
||||
**Que faire** :
|
||||
|
||||
- Vérifiez que vous utilisez le bon nom d'utilisateur et le bon mot de passe. Ceux-ci sont sensibles à la casse (majuscules/minuscules).
|
||||
- Si vous avez oublié votre mot de passe, utilisez la fonction de réinitialisation du mot de passe sur la page de connexion (si disponible sur votre instance).
|
||||
- Contactez votre administrateur ou le support InnovA AI si vous ne parvenez pas à récupérer l'accès à votre compte.
|
||||
|
||||
### Compte verrouillé
|
||||
|
||||
Si votre compte semble verrouillé ou désactivé :
|
||||
|
||||
**Que faire** :
|
||||
|
||||
- Contactez votre administrateur DictIA pour vérifier le statut de votre compte.
|
||||
|
||||
---
|
||||
|
||||
## Avis d'enregistrement (conformité légale)
|
||||
|
||||
Dans de nombreuses juridictions, vous devez informer les participants qu'ils sont enregistrés. DictIA permet d'afficher un avis de consentement avant tout enregistrement.
|
||||
|
||||
Si l'avis d'enregistrement ne s'affiche pas alors qu'il devrait :
|
||||
|
||||
**Que faire** :
|
||||
|
||||
- Contactez votre administrateur ou le support InnovA AI pour activer et configurer l'avis d'enregistrement dans les paramètres système.
|
||||
|
||||
---
|
||||
|
||||
## Besoin d'aide supplémentaire?
|
||||
|
||||
Si les solutions ci-dessus ne résolvent pas votre problème, contactez le support InnovA AI en fournissant :
|
||||
|
||||
- La description du problème et les étapes pour le reproduire.
|
||||
- Le navigateur et l'appareil utilisés.
|
||||
- Des captures d'écran si possible.
|
||||
- Votre version de DictIA (disponible dans *Paramètres* > *À propos*).
|
||||
49
client_docs/depannage/index.md
Normal file
49
client_docs/depannage/index.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Dépannage
|
||||
|
||||
Cette section vous aide à identifier et résoudre les problèmes courants que vous pourriez rencontrer avec DictIA. Les guides sont organisés par catégorie pour vous permettre de trouver rapidement la solution appropriée.
|
||||
|
||||
---
|
||||
|
||||
## Guides de dépannage
|
||||
|
||||
### [Problèmes de transcription](transcription.md)
|
||||
|
||||
Résolvez les problèmes liés à la transcription de vos enregistrements :
|
||||
|
||||
- Qualité de transcription insuffisante
|
||||
- Problèmes de langue et de détection automatique
|
||||
- Vocabulaire personnalisé et termes spécialisés
|
||||
- Résumés qui ne se génèrent pas correctement
|
||||
- Formats audio non reconnus
|
||||
|
||||
### [Problèmes de performance](performance.md)
|
||||
|
||||
Résolvez les problèmes de lenteur et de traitement :
|
||||
|
||||
- Interface web lente ou saccadée
|
||||
- Fichiers volumineux et délais de traitement
|
||||
- Enregistrements bloqués dans la file d'attente
|
||||
- Délais d'expiration sur les longs enregistrements
|
||||
|
||||
### [Problèmes de fonctionnalités](fonctionnalites.md)
|
||||
|
||||
Résolvez les problèmes liés aux fonctionnalités spécifiques :
|
||||
|
||||
- Identification des locuteurs
|
||||
- Partage d'enregistrements
|
||||
- Recherche IA (recherche sémantique)
|
||||
- Application mobile
|
||||
- Connexion et accès au compte
|
||||
|
||||
---
|
||||
|
||||
## Informations utiles pour le support
|
||||
|
||||
Si les solutions proposées ne résolvent pas votre problème, contactez le support InnovA AI en fournissant les informations suivantes :
|
||||
|
||||
- **Version de DictIA** : Disponible dans l'onglet *À propos* des paramètres de votre compte.
|
||||
- **Description du problème** : Décrivez les étapes que vous avez suivies et le comportement observé.
|
||||
- **Messages d'erreur** : Notez tout message d'erreur affiché à l'écran.
|
||||
- **Navigateur utilisé** : Indiquez le navigateur et sa version (ex. : Chrome 120, Firefox 121).
|
||||
|
||||
**Contact** : Communiquez avec l'équipe InnovA AI via les canaux de support mis à votre disposition par votre administrateur.
|
||||
93
client_docs/depannage/performance.md
Normal file
93
client_docs/depannage/performance.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# Dépannage : Performance
|
||||
|
||||
Ce guide couvre les problèmes de performance courants dans DictIA et les solutions pour les résoudre.
|
||||
|
||||
---
|
||||
|
||||
## Transcription lente
|
||||
|
||||
Le temps de traitement d'une transcription dépend de plusieurs facteurs :
|
||||
|
||||
- **Durée de l'enregistrement** : Plus le fichier audio est long, plus le traitement prend de temps. Un enregistrement d'une heure prend naturellement plus de temps qu'un enregistrement de 5 minutes.
|
||||
- **Taille du fichier** : Les fichiers volumineux nécessitent plus de temps de transfert et de traitement.
|
||||
- **Charge du système** : Si plusieurs utilisateurs téléversent des enregistrements simultanément, les temps de traitement augmentent.
|
||||
- **Modèle de transcription** : Les modèles plus précis sont généralement plus lents. Un modèle optimisé pour la vitesse traite plus rapidement mais peut être légèrement moins précis.
|
||||
|
||||
**Que faire** :
|
||||
|
||||
- Patientez. La plupart des transcriptions se complètent en quelques minutes pour des enregistrements de durée normale (moins de 30 minutes).
|
||||
- Pour les fichiers très volumineux, prévoyez un temps de traitement plus long.
|
||||
- Si les transcriptions sont systématiquement très lentes, contactez le support InnovA AI.
|
||||
|
||||
---
|
||||
|
||||
## Fichiers volumineux qui échouent
|
||||
|
||||
Certains fichiers très volumineux (plus de 25 Mo) peuvent échouer lors du traitement si le service de transcription impose des limites de taille.
|
||||
|
||||
**Que faire** :
|
||||
|
||||
- DictIA peut être configuré pour découper automatiquement les fichiers volumineux en segments plus petits. Si vos fichiers échouent systématiquement, contactez le support InnovA AI pour activer ou ajuster le découpage automatique.
|
||||
- Si possible, compressez vos fichiers audio avant de les téléverser (par exemple, convertissez un fichier WAV en MP3).
|
||||
|
||||
---
|
||||
|
||||
## Délai d'expiration sur les longs enregistrements
|
||||
|
||||
Les enregistrements très longs (plus de 30 minutes) peuvent dépasser le délai d'attente maximal du service de transcription.
|
||||
|
||||
**Que faire** :
|
||||
|
||||
- Si un enregistrement long échoue avec un message de délai d'expiration, contactez le support InnovA AI pour ajuster le paramètre de délai maximal.
|
||||
- En attendant, vous pouvez découper votre enregistrement en segments plus courts (par exemple, par tranches de 30 minutes) et les téléverser séparément.
|
||||
|
||||
---
|
||||
|
||||
## Enregistrements bloqués après un redémarrage
|
||||
|
||||
Si le système a été redémarré pendant le traitement d'enregistrements, ceux-ci reprennent automatiquement au prochain démarrage. La file d'attente est persistante, donc aucun téléversement n'est perdu.
|
||||
|
||||
**Que faire** :
|
||||
|
||||
- Patientez quelques minutes après un redémarrage pour que les enregistrements en cours reprennent leur traitement.
|
||||
- Si un enregistrement reste bloqué plus de 30 minutes après un redémarrage, contactez le support InnovA AI.
|
||||
|
||||
---
|
||||
|
||||
## L'interface web est lente ou saccadée
|
||||
|
||||
La performance du navigateur peut se dégrader dans certaines situations :
|
||||
|
||||
### Transcriptions très longues
|
||||
|
||||
Les enregistrements de plus de 2 heures génèrent un volume important de texte que certains navigateurs peinent à afficher de manière fluide. L'affichage en mode « bulles » pour les transcriptions avec identification des locuteurs est particulièrement exigeant en ressources.
|
||||
|
||||
**Que faire** :
|
||||
|
||||
- Utilisez l'affichage simple plutôt que l'affichage en bulles pour les transcriptions très longues.
|
||||
- Fermez les autres onglets de votre navigateur pour libérer de la mémoire.
|
||||
|
||||
### Cache du navigateur
|
||||
|
||||
Le cache local peut devenir volumineux ou corrompu avec le temps, ce qui ralentit l'interface.
|
||||
|
||||
**Que faire** :
|
||||
|
||||
- Effectuez un rafraîchissement complet de la page avec **Ctrl+Maj+R** (Windows) ou **Cmd+Maj+R** (Mac).
|
||||
- Videz le cache de votre navigateur si le problème persiste.
|
||||
|
||||
### Navigateur et appareil
|
||||
|
||||
- Assurez-vous d'utiliser une version récente de votre navigateur (Chrome, Firefox, Edge ou Safari).
|
||||
- Sur les appareils mobiles ou les ordinateurs plus anciens, la performance peut être réduite avec de très longues transcriptions.
|
||||
|
||||
---
|
||||
|
||||
## Besoin d'aide supplémentaire?
|
||||
|
||||
Si les solutions ci-dessus ne résolvent pas votre problème, contactez le support InnovA AI en fournissant :
|
||||
|
||||
- La description du problème et les étapes pour le reproduire.
|
||||
- Le navigateur et l'appareil utilisés.
|
||||
- La durée approximative de l'enregistrement concerné.
|
||||
- Votre version de DictIA (disponible dans *Paramètres* > *À propos*).
|
||||
136
client_docs/depannage/transcription.md
Normal file
136
client_docs/depannage/transcription.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# Dépannage : Transcription
|
||||
|
||||
Ce guide couvre les problèmes courants liés à la transcription de vos enregistrements dans DictIA et les solutions à appliquer.
|
||||
|
||||
---
|
||||
|
||||
## La transcription ne démarre pas
|
||||
|
||||
Lorsqu'un enregistrement reste au statut « En attente » ou « En file d'attente » pendant une période prolongée, voici les causes possibles :
|
||||
|
||||
**Statut « En file d'attente »** : Les enregistrements affichent ce statut lorsqu'ils attendent un processus de traitement disponible. Si plusieurs enregistrements sont en file d'attente, ils seront traités dans un ordre équitable entre les utilisateurs. Aucun utilisateur ne peut monopoliser la file d'attente.
|
||||
|
||||
**Que faire** :
|
||||
|
||||
- Patientez quelques minutes. Si votre instance est partagée avec d'autres utilisateurs, votre enregistrement sera traité à son tour.
|
||||
- Si l'enregistrement reste bloqué plus de 30 minutes, contactez le support InnovA AI.
|
||||
|
||||
---
|
||||
|
||||
## La transcription échoue immédiatement
|
||||
|
||||
Un échec rapide de la transcription indique généralement un problème de configuration côté serveur.
|
||||
|
||||
**Que faire** :
|
||||
|
||||
- Vérifiez que votre fichier audio n'est pas corrompu en l'écoutant dans un autre lecteur.
|
||||
- Réessayez le téléversement de votre fichier.
|
||||
- Si le problème persiste, contactez le support InnovA AI.
|
||||
|
||||
---
|
||||
|
||||
## Qualité de transcription insuffisante
|
||||
|
||||
La précision de la transcription dépend fortement de la qualité audio. Voici les facteurs qui influencent les résultats :
|
||||
|
||||
### Qualité audio
|
||||
|
||||
- **Bruit de fond** : Le bruit ambiant, la musique ou les conversations en arrière-plan dégradent considérablement les résultats.
|
||||
- **Chevauchement des voix** : Lorsque plusieurs personnes parlent en même temps, la transcription devient imprécise.
|
||||
- **Microphone** : Un mauvais positionnement du microphone ou un micro de faible qualité affecte la clarté.
|
||||
|
||||
**Bonnes pratiques pour un enregistrement de qualité** :
|
||||
|
||||
- Enregistrez dans un environnement calme.
|
||||
- Utilisez un microphone de bonne qualité, positionné près des intervenants.
|
||||
- Pour les réunions, placez le micro au centre de la table.
|
||||
- Évitez la musique de fond ou les bruits parasites.
|
||||
|
||||
### Problème de langue
|
||||
|
||||
Si la langue de transcription configurée ne correspond pas à la langue réellement parlée dans l'enregistrement, la précision diminue fortement.
|
||||
|
||||
**Que faire** :
|
||||
|
||||
- Vérifiez le paramètre de langue dans vos réglages de compte, sous *Préférences de langue*.
|
||||
- Vous pouvez sélectionner une langue spécifique ou laisser la détection automatique si vos enregistrements sont dans des langues variées.
|
||||
- Pour la transcription en chinois, assurez-vous que le modèle utilisé est compatible. Certains modèles simplifiés ne prennent pas correctement en charge le chinois. Si vous obtenez du texte romanisé au lieu de caractères chinois, contactez le support InnovA AI.
|
||||
|
||||
---
|
||||
|
||||
## Termes spécialisés mal transcrits
|
||||
|
||||
Il est fréquent que les noms propres, acronymes, termes techniques ou noms de marque soient mal transcrits. DictIA offre deux fonctionnalités pour corriger cela :
|
||||
|
||||
### Vocabulaire personnalisé
|
||||
|
||||
Fournissez une liste de termes séparés par des virgules que le moteur de transcription doit prioriser. Par exemple, si « PyAnnote » est systématiquement transcrit comme « piano » ou « pie annotate », ajoutez-le à votre vocabulaire personnalisé.
|
||||
|
||||
### Indice de transcription (Initial Prompt)
|
||||
|
||||
Donnez au moteur de transcription un contexte plus large sur le contenu de votre enregistrement, ce qui l'aide à faire de meilleurs choix lexicaux.
|
||||
|
||||
### Niveaux de configuration
|
||||
|
||||
Ces paramètres peuvent être définis à plusieurs niveaux, du plus prioritaire au moins prioritaire :
|
||||
|
||||
1. **Par téléversement** : Dans les options avancées lors du téléversement d'un fichier.
|
||||
2. **Par étiquette** : Dans les paramètres d'une étiquette (appliqué automatiquement lorsque cette étiquette est sélectionnée).
|
||||
3. **Par dossier** : Dans les paramètres d'un dossier (appliqué à tous les enregistrements du dossier).
|
||||
4. **Par utilisateur** : Dans les *Paramètres du compte*, section *Options de transcription* (vos valeurs par défaut personnelles).
|
||||
|
||||
---
|
||||
|
||||
## Erreur « Format non reconnu »
|
||||
|
||||
Si vous recevez une erreur indiquant que le format audio n'est pas reconnu, cela signifie que le service de transcription ne prend pas en charge le codec de votre fichier.
|
||||
|
||||
**Que faire** :
|
||||
|
||||
- Convertissez votre fichier audio dans un format plus courant (MP3 ou WAV) avant de le téléverser.
|
||||
- Si le problème se produit fréquemment, contactez le support InnovA AI pour qu'une solution automatique soit configurée.
|
||||
|
||||
---
|
||||
|
||||
## Le résumé ne se génère pas
|
||||
|
||||
Si aucun résumé n'est produit après la transcription :
|
||||
|
||||
**Que faire** :
|
||||
|
||||
- Vérifiez que la transcription elle-même a été complétée avec succès.
|
||||
- Essayez de retraiter le résumé en cliquant sur « Retraiter le résumé » dans la page de l'enregistrement.
|
||||
- Si le problème persiste, contactez le support InnovA AI.
|
||||
|
||||
---
|
||||
|
||||
## La langue du résumé ne correspond pas à vos préférences
|
||||
|
||||
Si les résumés sont générés dans une langue différente de celle configurée dans vos préférences :
|
||||
|
||||
**Que faire** :
|
||||
|
||||
- Vérifiez vos préférences de langue dans *Paramètres du compte* > *Préférences de langue*.
|
||||
- Essayez de retraiter le résumé après avoir confirmé vos préférences.
|
||||
- Si le problème persiste malgré des préférences correctes, contactez le support InnovA AI. Ce comportement peut être lié au modèle IA utilisé par votre instance.
|
||||
|
||||
---
|
||||
|
||||
## Transcription modifiable
|
||||
|
||||
Sachez que les transcriptions sont entièrement modifiables après leur génération. Si vous constatez des erreurs :
|
||||
|
||||
- Cliquez sur le bouton *Modifier* au-dessus de la transcription.
|
||||
- Corrigez les termes mal transcrits, les noms propres ou les attributions de locuteurs.
|
||||
- Vos modifications sont préservées, même si vous retraitez le résumé ou utilisez la Conversation IA.
|
||||
|
||||
---
|
||||
|
||||
## Besoin d'aide supplémentaire?
|
||||
|
||||
Si les solutions ci-dessus ne résolvent pas votre problème, contactez le support InnovA AI en fournissant :
|
||||
|
||||
- La description du problème et les étapes pour le reproduire.
|
||||
- Le format et la durée approximative de votre fichier audio.
|
||||
- Tout message d'erreur affiché à l'écran.
|
||||
- Votre version de DictIA (disponible dans *Paramètres* > *À propos*).
|
||||
227
client_docs/guide-admin/gestion-groupes.md
Normal file
227
client_docs/guide-admin/gestion-groupes.md
Normal file
@@ -0,0 +1,227 @@
|
||||
# Gestion des groupes
|
||||
|
||||
Les groupes permettent d'organiser la collaboration dans votre instance DictIA en regroupant les utilisateurs et en automatisant l'accès aux enregistrements grâce à des étiquettes de groupe. Cette fonctionnalité réduit la charge administrative tout en maintenant la sécurité et le contrôle sur l'accès au contenu.
|
||||
|
||||
## Prérequis
|
||||
|
||||
La fonctionnalité de groupes repose sur le partage interne de DictIA. Le partage interne et la visibilité des noms d'utilisateur doivent être activés pour que l'onglet Groupes apparaisse dans le tableau de bord d'administration. Contactez le support InnovA AI pour ajuster ces paramètres.
|
||||
|
||||
### Considérations de confidentialité
|
||||
|
||||
La visibilité des noms d'utilisateur affecte l'ensemble de l'instance :
|
||||
|
||||
- **Noms visibles** : Les utilisateurs voient les noms d'utilisateur réels lorsqu'ils recherchent des collègues et consultent du contenu partagé. Recommandé pour les petits groupes de confiance.
|
||||
- **Noms masqués** : Les noms d'utilisateur sont cachés de l'interface. Les utilisateurs doivent connaître le nom d'utilisateur exact de leurs collègues pour partager des enregistrements. Approche axée sur la confidentialité, conforme à la Loi 25.
|
||||
|
||||
La fonctionnalité de groupe fonctionne de manière identique dans les deux modes; seul l'affichage change.
|
||||
|
||||
## Créer et gérer des groupes
|
||||
|
||||
Les groupes sont créés et gérés exclusivement par les administrateurs via le tableau de bord d'administration.
|
||||
|
||||
### Créer un groupe
|
||||
|
||||
1. Accédez au **Tableau de bord d'administration** et sélectionnez l'onglet **Groupes**
|
||||
2. Cliquez sur **Créer un groupe**
|
||||
3. Fournissez un nom qui identifie clairement l'objectif du groupe (ex. : "Ingénierie", "Ventes Québec", "Projet Alpha")
|
||||
4. Ajoutez une description (optionnel mais recommandé) expliquant le rôle du groupe
|
||||
5. Cliquez sur **Créer un groupe** pour finaliser
|
||||
|
||||
Le groupe apparaît immédiatement dans la liste, sans aucun membre. Vous devez ajouter explicitement les membres, y compris vous-même si nécessaire.
|
||||
|
||||
### Gérer les membres
|
||||
|
||||
Cliquez sur l'icône de gestion des utilisateurs à côté d'un groupe pour ouvrir la fenêtre de gestion. Cette interface affiche les membres actuels, leurs rôles et fournit les outils pour ajouter ou retirer des membres.
|
||||
|
||||
Pour ajouter un membre, sélectionnez un utilisateur dans le menu déroulant et choisissez son rôle :
|
||||
|
||||
- **Membre** : Peut utiliser les étiquettes de groupe et accéder aux enregistrements étiquetés. Convient à la plupart des participants.
|
||||
- **Administrateur de groupe** : Toutes les capacités d'un membre, plus la gestion des membres du groupe, la création et suppression d'étiquettes de groupe, et l'accès aux fonctions de gestion du groupe.
|
||||
|
||||
Cliquez sur **Ajouter un membre** pour accorder l'accès. L'utilisateur obtient immédiatement la visibilité sur les étiquettes du groupe. Il n'obtient pas automatiquement l'accès aux enregistrements existants, uniquement aux nouveaux enregistrements étiquetés après son ajout.
|
||||
|
||||
### Modifier les rôles des membres
|
||||
|
||||
Les rôles peuvent évoluer selon les responsabilités. Cliquez sur le menu déroulant de rôle à côté d'un membre pour le basculer entre Membre et Administrateur de groupe. Les changements prennent effet immédiatement.
|
||||
|
||||
### Retirer des membres
|
||||
|
||||
Cliquez sur l'icône de suppression à côté d'un membre pour le retirer du groupe. Le retrait est immédiat :
|
||||
|
||||
- L'utilisateur perd l'accès aux étiquettes de groupe
|
||||
- Il ne recevra plus les nouveaux enregistrements étiquetés
|
||||
- Son accès aux enregistrements déjà partagés est conservé
|
||||
- Ses notes personnelles sur les enregistrements du groupe sont préservées
|
||||
|
||||
Pour révoquer complètement l'accès aux enregistrements existants, vous devez révoquer manuellement les partages internes individuels.
|
||||
|
||||
### Supprimer un groupe
|
||||
|
||||
Cliquez sur l'icône de suppression à côté d'un groupe pour le supprimer entièrement. Une confirmation est demandée. La suppression :
|
||||
|
||||
- Retire tous les membres
|
||||
- Supprime toutes les étiquettes du groupe
|
||||
- Préserve tous les enregistrements (y compris ceux précédemment étiquetés)
|
||||
- Préserve tous les partages internes créés par les étiquettes
|
||||
|
||||
La suppression est irréversible. Si vous souhaitez temporairement désactiver un groupe, retirez plutôt tous les membres.
|
||||
|
||||
## Étiquettes de groupe
|
||||
|
||||
Les étiquettes de groupe automatisent le partage au sein des groupes. Contrairement aux étiquettes personnelles qui organisent le contenu individuel, les étiquettes de groupe déclenchent des autorisations d'accès pour tous les membres du groupe lorsqu'elles sont appliquées.
|
||||
|
||||
### Créer des étiquettes de groupe
|
||||
|
||||
Depuis l'onglet Groupes, cliquez sur l'icône d'étiquettes à côté du groupe concerné. Fournissez :
|
||||
|
||||
- **Nom de l'étiquette** : Un nom décrivant le type de contenu ou l'objectif (ex. : "Revues de sprint", "Appels clients", "Contrats juridiques"). Évitez les noms génériques comme "Important" ou "Groupe".
|
||||
- **Couleur** : Pour distinguer visuellement l'étiquette. Établissez des conventions de couleurs (ex. : bleu pour le technique, vert pour les ventes, rouge pour le juridique).
|
||||
|
||||
### Politiques de rétention par étiquette
|
||||
|
||||
Les étiquettes de groupe peuvent remplacer les paramètres globaux de rétention avec des périodes spécifiques. Cette fonctionnalité permet à différents types de contenu d'avoir des cycles de vie différents.
|
||||
|
||||
- **Champ vide** : Utilise les paramètres globaux de rétention
|
||||
- **Nombre de jours** : Les enregistrements avec cette étiquette seront supprimés automatiquement après la période spécifiée
|
||||
|
||||
Exemples de configuration :
|
||||
|
||||
| Type de contenu | Rétention | Justification |
|
||||
|----------------|-----------|---------------|
|
||||
| Réunions juridiques | 2555 jours (7 ans) | Conformité et archivage légal |
|
||||
| Réunions quotidiennes | 14 jours | Contenu éphémère quotidien |
|
||||
| Planification marketing | 180 jours | Cycle de planification de campagne |
|
||||
|
||||
Lorsqu'un enregistrement possède plusieurs étiquettes avec différentes périodes de rétention, la période la **plus courte** s'applique. Cela garantit la conformité avec la politique la plus restrictive, tel que recommandé par la Loi 25.
|
||||
|
||||
### Protection contre la suppression
|
||||
|
||||
Activez **Protéger contre la suppression** pour rendre les enregistrements avec cette étiquette immunisés contre la suppression automatique, peu importe leur âge ou les paramètres de rétention.
|
||||
|
||||
Utilisez la protection pour les enregistrements qui doivent être conservés indéfiniment :
|
||||
|
||||
- Documents juridiques et de conformité
|
||||
- Décisions d'affaires critiques
|
||||
- Matériel de formation et d'intégration
|
||||
- Documentation de référence
|
||||
|
||||
La protection peut être retirée ultérieurement en modifiant l'étiquette.
|
||||
|
||||
### Paramètres de partage automatique
|
||||
|
||||
Les étiquettes de groupe offrent deux niveaux de partage automatique qui se déclenchent lorsqu'un membre du groupe applique l'étiquette :
|
||||
|
||||
- **Partager avec tous les membres du groupe** (par défaut et recommandé) : L'enregistrement est partagé avec tous les membres du groupe, en mode consultation et modification.
|
||||
- **Partager avec les administrateurs de groupe seulement** : Seuls les utilisateurs avec le rôle administrateur dans ce groupe reçoivent automatiquement l'accès. Convient au contenu sensible nécessitant une supervision.
|
||||
|
||||
### Gérer les étiquettes existantes
|
||||
|
||||
Les étiquettes existantes apparaissent dans la fenêtre de gestion avec leurs paramètres actuels. Cliquez sur l'icône de modification pour ajuster le nom, la couleur, la rétention, la protection ou les paramètres de partage. Les modifications s'appliquent aux futurs usages sans affecter rétroactivement les partages déjà effectués.
|
||||
|
||||
La suppression d'une étiquette la retire de tous les enregistrements, mais ne supprime ni les enregistrements ni les accès déjà accordés.
|
||||
|
||||
### Synchroniser les partages de groupe
|
||||
|
||||
Si des membres ont été ajoutés après que des enregistrements aient déjà été étiquetés, la fonctionnalité **Synchroniser les partages de groupe** permet d'appliquer rétroactivement le partage automatique.
|
||||
|
||||
Cliquez sur **Synchroniser les partages de groupe** dans la fenêtre de gestion. L'opération :
|
||||
|
||||
- Identifie tous les enregistrements avec les étiquettes du groupe
|
||||
- Vérifie les partages existants avec les membres actuels
|
||||
- Crée les partages manquants
|
||||
- Respecte les paramètres de partage de chaque étiquette
|
||||
- Évite les doublons
|
||||
|
||||
La synchronisation est sécuritaire et peut être exécutée plusieurs fois sans effet indésirable.
|
||||
|
||||
## Rôle d'administrateur de groupe
|
||||
|
||||
Les administrateurs de groupe sont des membres avec des permissions élevées, limitées à la portée de leur groupe.
|
||||
|
||||
### Capacités
|
||||
|
||||
Les administrateurs de groupe peuvent :
|
||||
|
||||
- Ajouter et retirer des membres
|
||||
- Modifier les rôles des membres
|
||||
- Créer, modifier et supprimer des étiquettes de groupe
|
||||
- Synchroniser les partages du groupe
|
||||
|
||||
Les administrateurs de groupe ne peuvent **pas** :
|
||||
|
||||
- Créer ou supprimer des groupes
|
||||
- Gérer des groupes dont ils ne sont pas administrateurs
|
||||
- Accéder aux fonctionnalités d'administration du système
|
||||
- Voir les statistiques d'autres groupes
|
||||
|
||||
Cette délégation permet de distribuer la gestion aux responsables d'équipe tout en maintenant le contrôle centralisé.
|
||||
|
||||
## Bonnes pratiques
|
||||
|
||||
### Structure des groupes
|
||||
|
||||
**Petites organisations (moins de 10 utilisateurs)** : Créez des groupes par département (Ingénierie, Ventes, RH). Utilisez les étiquettes de groupe pour les noms de projets ou types de contenu.
|
||||
|
||||
**Grandes organisations (plus de 10 utilisateurs)** : Créez des groupes par produit, division ou projet majeur. Des groupes plus sélectifs évitent la surcharge d'information.
|
||||
|
||||
### Conventions de nommage des étiquettes
|
||||
|
||||
Établissez des conventions dès le début :
|
||||
|
||||
- Par projet : "Projet-Alpha", "Initiative-T3-2026"
|
||||
- Par type de contenu : "Revues-Sprint", "Appels-Clients", "Contrats-Juridiques"
|
||||
- Par département : "Ing-Architecture", "Ventes-Formation", "RH-Entrevues"
|
||||
|
||||
### Stratégie de rétention
|
||||
|
||||
Définissez des valeurs par défaut réfléchies qui équilibrent les coûts de stockage avec les besoins de conformité, particulièrement en regard de la Loi 25 :
|
||||
|
||||
- **Rétention globale** : 90 jours (capture la plupart du contenu)
|
||||
- **Étiquettes juridiques** : 2555 jours (7 ans pour les archives légales)
|
||||
- **Étiquettes de conformité** : Protégées (rétention permanente)
|
||||
- **Réunions d'équipe** : 180 jours (fenêtre de collaboration raisonnable)
|
||||
- **Réunions quotidiennes** : 14 jours (contenu éphémère)
|
||||
|
||||
Révisez vos politiques de rétention chaque trimestre.
|
||||
|
||||
## Intégration avec les autres fonctionnalités
|
||||
|
||||
### Recherche IA
|
||||
|
||||
Les étiquettes de groupe apparaissent automatiquement dans les filtres de la Recherche IA, permettant une recherche sémantique ciblée par groupe. Les enregistrements partagés via les étiquettes de groupe sont inclus dans les résultats de recherche sémantique.
|
||||
|
||||
### Rétention et suppression automatique
|
||||
|
||||
Les politiques de rétention par étiquette s'intègrent au système de suppression automatique de DictIA :
|
||||
|
||||
1. Si l'enregistrement a des étiquettes protégées → Jamais supprimé
|
||||
2. Si l'enregistrement a des étiquettes avec rétention personnalisée → La période la plus courte s'applique
|
||||
3. Sinon → La rétention globale s'applique
|
||||
|
||||
### Partage public
|
||||
|
||||
L'appartenance à un groupe n'affecte pas les capacités de partage public. La capacité d'un utilisateur à créer des liens de partage public est contrôlée indépendamment par ses permissions individuelles.
|
||||
|
||||
---
|
||||
|
||||
## Dépannage
|
||||
|
||||
### L'onglet Groupes n'est pas visible
|
||||
|
||||
Le partage interne n'est peut-être pas activé. Contactez le support InnovA AI pour vérifier la configuration.
|
||||
|
||||
### Les utilisateurs ne voient pas les étiquettes de groupe
|
||||
|
||||
Vérifiez que l'utilisateur est bien membre du groupe et qu'il est connecté. Rafraîchissez la page pour charger les listes d'étiquettes mises à jour.
|
||||
|
||||
### Le partage automatique ne fonctionne pas
|
||||
|
||||
Modifiez l'étiquette de groupe et vérifiez que l'option de partage est bien activée. Si le problème persiste, contactez le support InnovA AI.
|
||||
|
||||
### Un administrateur de groupe ne peut pas accéder à l'interface de gestion
|
||||
|
||||
Vérifiez que le rôle de l'utilisateur est bien "Administrateur" dans le groupe. Demandez-lui de se déconnecter et de se reconnecter pour rafraîchir sa session. Le lien **Gestion des groupes** devrait apparaître dans son menu utilisateur.
|
||||
|
||||
---
|
||||
|
||||
Retour au [Guide d'administration](index.md) →
|
||||
84
client_docs/guide-admin/gestion-utilisateurs.md
Normal file
84
client_docs/guide-admin/gestion-utilisateurs.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# Gestion des utilisateurs
|
||||
|
||||
La section Gestion des utilisateurs est le centre de contrôle pour gérer les accès à votre instance DictIA et les permissions de chaque utilisateur. Chaque compte utilisateur est géré ici, de la création initiale jusqu'à la désactivation éventuelle. Pour les tendances d'utilisation à l'échelle du système, consultez les [statistiques du système](statistiques.md).
|
||||
|
||||
## Comprendre le tableau des utilisateurs
|
||||
|
||||
En ouvrant la section Gestion des utilisateurs, vous accédez à un tableau complet de tous les utilisateurs de votre système. Chaque ligne présente les informations essentielles : l'adresse courriel identifie l'utilisateur, le badge administrateur indique son niveau de privilège, le nombre d'enregistrements révèle son niveau d'activité, et la mesure du stockage montre sa consommation de ressources.
|
||||
|
||||
La barre de recherche en haut du tableau filtre les résultats instantanément à mesure que vous tapez, ce qui facilite la recherche d'utilisateurs spécifiques. Le tableau se met à jour en temps réel lorsque vous apportez des modifications.
|
||||
|
||||
## Ajouter un nouvel utilisateur
|
||||
|
||||
Pour créer un compte utilisateur, cliquez sur le bouton **Ajouter un utilisateur** dans le coin supérieur droit. La fenêtre qui apparaît demande les informations essentielles : nom d'utilisateur, adresse courriel et mot de passe. Vous décidez également immédiatement si cette personne a besoin de privilèges d'administrateur, bien que vous puissiez toujours modifier cela ultérieurement.
|
||||
|
||||
Le nom d'utilisateur devient l'identité de l'utilisateur dans DictIA, apparaissant dans l'interface et organisant ses enregistrements. L'adresse courriel sert à la fois d'identifiant de connexion et de point de contact. Le mot de passe que vous définissez est temporaire; les utilisateurs devraient le changer immédiatement après leur première connexion via leurs [paramètres de compte](../guide-utilisateur/parametres.md).
|
||||
|
||||
Les privilèges d'administrateur sont puissants et devraient être accordés avec parcimonie. Les utilisateurs administrateurs peuvent voir et modifier tous les [paramètres du système](parametres-systeme.md), gérer les autres utilisateurs, configurer les [consignes par défaut](prompts.md) et surveiller la [recherche sémantique](recherche-semantique.md). La plupart des utilisateurs n'auront jamais besoin de ces capacités.
|
||||
|
||||
### Permissions des utilisateurs
|
||||
|
||||
Au-delà du rôle d'administrateur, DictIA offre des permissions granulaires par utilisateur qui contrôlent des capacités spécifiques.
|
||||
|
||||
**Permission de partage public** : Le bouton d'icône globe dans la ligne de chaque utilisateur active ou désactive sa capacité à créer des liens de partage public. Lorsque cette option est activée (vert), l'utilisateur peut générer des liens sécurisés pour partager des enregistrements à l'externe avec n'importe qui. Lorsqu'elle est désactivée (gris), l'utilisateur ne peut utiliser que le partage interne avec d'autres utilisateurs DictIA.
|
||||
|
||||
Utilisez cette permission pour contrôler le partage d'informations à l'externe par utilisateur. Vous pourriez accorder le partage public aux gestionnaires, aux équipes de vente ou au personnel marketing qui communiquent régulièrement avec des parties prenantes externes, tout en le restreignant pour les utilisateurs qui manipulent des données internes sensibles.
|
||||
|
||||
### Gestion du budget de jetons IA
|
||||
|
||||
Contrôlez les coûts d'IA en définissant des budgets mensuels de jetons pour chaque utilisateur. Lors de la création ou de la modification d'un utilisateur, vous pouvez spécifier une limite mensuelle de jetons qui restreint son utilisation de l'IA.
|
||||
|
||||
**Fonctionnement des budgets de jetons** :
|
||||
|
||||
- **Avertissement à 80 %** : Les utilisateurs voient un indicateur jaune lorsqu'ils atteignent 80 % de leur budget mensuel
|
||||
- **Bloqué à 100 %** : Une fois le budget épuisé, les opérations d'IA (conversation IA, résumé, etc.) sont bloquées jusqu'au mois suivant
|
||||
- **Réinitialisation du budget** : Les compteurs se réinitialisent automatiquement au début de chaque mois civil
|
||||
- **Sans limite** : Laissez le champ de budget vide pour une utilisation illimitée
|
||||
|
||||
Les budgets de jetons sont définis par incréments de 10 000 jetons avec un minimum de 100 000. Le budget couvre toutes les opérations d'IA, y compris la génération de résumés, la conversation IA, la génération de titres et l'extraction d'événements. Consultez les statistiques détaillées d'utilisation des jetons dans la section [Statistiques du système](statistiques.md#statistiques-dutilisation-des-jetons).
|
||||
|
||||
### Gestion du budget de transcription
|
||||
|
||||
Contrôlez les coûts de transcription en définissant des budgets mensuels de transcription (en minutes) pour chaque utilisateur.
|
||||
|
||||
**Fonctionnement des budgets de transcription** :
|
||||
|
||||
- **Avertissement à 80 %** : Les utilisateurs voient un indicateur jaune lorsqu'ils atteignent 80 % de leur budget mensuel de transcription
|
||||
- **Bloqué à 100 %** : Une fois le budget épuisé, les nouvelles transcriptions sont bloquées jusqu'au mois suivant
|
||||
- **Réinitialisation du budget** : Les minutes de transcription se réinitialisent automatiquement au début de chaque mois civil
|
||||
- **Sans limite** : Laissez le champ de budget vide pour une utilisation illimitée de la transcription
|
||||
|
||||
Les budgets de transcription sont définis en minutes avec un minimum de 10 minutes. Consultez les statistiques détaillées de transcription dans la section [Statistiques du système](statistiques.md#statistiques-dutilisation-de-la-transcription).
|
||||
|
||||
## Gérer les utilisateurs existants
|
||||
|
||||
Chaque ligne d'utilisateur comprend des boutons d'action qui vous donnent un contrôle complet sur ce compte.
|
||||
|
||||
**Modifier** : Le bouton de modification ouvre une fenêtre où vous pouvez mettre à jour le nom d'utilisateur ou l'adresse courriel. Utile lorsqu'une personne change de nom, d'adresse courriel ou lorsque vous devez corriger des erreurs de saisie initiale.
|
||||
|
||||
**Basculer le rôle d'administrateur** : Promouvoir un utilisateur au rôle d'administrateur lui accorde l'accès à tout ce que vous pouvez voir et faire. Rétrograder un administrateur révoque immédiatement toutes ses capacités administratives. Le système vous empêche de retirer vos propres droits d'administrateur pour éviter tout verrouillage accidentel.
|
||||
|
||||
**Supprimer** : La suppression d'un utilisateur est permanente et ne peut pas être annulée. Tous ses enregistrements, notes et paramètres seront supprimés avec son compte. Le système demande une confirmation avant de procéder.
|
||||
|
||||
## Surveiller les tendances d'utilisation
|
||||
|
||||
Les colonnes de nombre d'enregistrements et de stockage révèlent comment les utilisateurs interagissent avec votre instance DictIA.
|
||||
|
||||
- Un nombre élevé d'enregistrements peut indiquer des utilisateurs avancés qui dépendent fortement du système
|
||||
- Un nombre faible peut suggérer des utilisateurs qui ont besoin de formation ou qui n'ont peut-être pas besoin de compte
|
||||
- Un stockage disproportionnellement élevé peut indiquer le téléversement d'enregistrements très longs
|
||||
|
||||
Ces informations vous aident à établir des politiques d'utilisation appropriées et à planifier la capacité du système.
|
||||
|
||||
## Considérations de sécurité
|
||||
|
||||
Chaque compte utilisateur est un vecteur de sécurité potentiel. Voici les bonnes pratiques à suivre :
|
||||
|
||||
- **Mots de passe robustes** : Encouragez les utilisateurs à utiliser des mots de passe uniques et à les changer régulièrement
|
||||
- **Comptes administrateurs** : Limitez l'accès administrateur au strict minimum nécessaire. Lorsqu'une personne n'a plus besoin de ces privilèges, révoquez-les immédiatement
|
||||
- **Comptes inactifs** : Effectuez des audits réguliers pour identifier et supprimer les comptes inactifs qui pourraient présenter des risques
|
||||
- **Conformité Loi 25** : Assurez-vous que seuls les utilisateurs légitimes ont accès aux enregistrements contenant des renseignements personnels
|
||||
|
||||
---
|
||||
|
||||
Suivant : [Statistiques du système](statistiques.md) →
|
||||
135
client_docs/guide-admin/index.md
Normal file
135
client_docs/guide-admin/index.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# Guide d'administration
|
||||
|
||||
Bienvenue dans le guide d'administration de DictIA. En tant qu'administrateur, vous contrôlez l'ensemble de votre instance DictIA : gestion des utilisateurs, surveillance du système et configuration de l'intelligence artificielle.
|
||||
|
||||
## Sections administratives
|
||||
|
||||
<div class="guide-cards">
|
||||
<div class="guide-card">
|
||||
<div class="card-icon">👥</div>
|
||||
<h3>Gestion des utilisateurs</h3>
|
||||
<p>Créez des comptes, gérez les permissions, surveillez l'utilisation et contrôlez l'accès à votre instance DictIA.</p>
|
||||
<a href="gestion-utilisateurs" class="card-link">Gérer les utilisateurs →</a>
|
||||
</div>
|
||||
|
||||
<div class="guide-card">
|
||||
<div class="card-icon">🤝</div>
|
||||
<h3>Gestion des groupes</h3>
|
||||
<p>Créez des groupes, attribuez des rôles, configurez les étiquettes de partage automatique et facilitez la collaboration.</p>
|
||||
<a href="gestion-groupes" class="card-link">Gérer les groupes →</a>
|
||||
</div>
|
||||
|
||||
<div class="guide-card">
|
||||
<div class="card-icon">📊</div>
|
||||
<h3>Statistiques du système</h3>
|
||||
<p>Surveillez la santé du système, suivez les tendances d'utilisation et identifiez les enjeux avant qu'ils n'affectent vos utilisateurs.</p>
|
||||
<a href="statistiques" class="card-link">Voir les statistiques →</a>
|
||||
</div>
|
||||
|
||||
<div class="guide-card">
|
||||
<div class="card-icon">🔧</div>
|
||||
<h3>Paramètres du système</h3>
|
||||
<p>Configurez les limites globales, les délais d'attente, les tailles de fichiers et les comportements qui s'appliquent à tous les utilisateurs.</p>
|
||||
<a href="parametres-systeme" class="card-link">Configurer le système →</a>
|
||||
</div>
|
||||
|
||||
<div class="guide-card">
|
||||
<div class="card-icon">🤖</div>
|
||||
<h3>Modèles d'IA</h3>
|
||||
<p>Découvrez les modèles d'intelligence artificielle utilisés pour la génération de résumés, la conversation IA et plus encore.</p>
|
||||
<a href="modeles-ia" class="card-link">Voir les modèles →</a>
|
||||
</div>
|
||||
|
||||
<div class="guide-card">
|
||||
<div class="card-icon">✨</div>
|
||||
<h3>Consignes par défaut</h3>
|
||||
<p>Personnalisez le comportement de l'IA avec des consignes de résumé qui façonnent le traitement du contenu.</p>
|
||||
<a href="prompts" class="card-link">Configurer les consignes →</a>
|
||||
</div>
|
||||
|
||||
<div class="guide-card">
|
||||
<div class="card-icon">🔍</div>
|
||||
<h3>Recherche sémantique</h3>
|
||||
<p>Gérez les capacités de recherche intelligente, surveillez l'état de l'indexation sémantique et contrôlez la Recherche IA.</p>
|
||||
<a href="recherche-semantique" class="card-link">Gérer la recherche →</a>
|
||||
</div>
|
||||
|
||||
<div class="guide-card">
|
||||
<div class="card-icon">🗑️</div>
|
||||
<h3>Rétention et suppression automatique</h3>
|
||||
<p>Configurez la gestion automatisée du cycle de vie des données avec des politiques de rétention flexibles, conformes à la Loi 25.</p>
|
||||
<a href="retention" class="card-link">Gérer la rétention →</a>
|
||||
</div>
|
||||
|
||||
<div class="guide-card">
|
||||
<div class="card-icon">🔐</div>
|
||||
<h3>Authentification unique (SSO)</h3>
|
||||
<p>Intégrez DictIA avec votre fournisseur d'identité pour simplifier la connexion de vos utilisateurs.</p>
|
||||
<a href="sso" class="card-link">Configurer le SSO →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
## Actions rapides
|
||||
|
||||
<div class="action-cards">
|
||||
<div class="action-card">
|
||||
<span class="action-icon">➕</span>
|
||||
<div>
|
||||
<strong>Ajouter un utilisateur</strong>
|
||||
<p>Gestion des utilisateurs → Bouton Ajouter → Saisir les informations → Définir les permissions</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-card">
|
||||
<span class="action-icon">🤝</span>
|
||||
<div>
|
||||
<strong>Créer un groupe</strong>
|
||||
<p>Gestion des groupes → Créer un groupe → Ajouter des membres → Configurer les étiquettes</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-card">
|
||||
<span class="action-icon">📈</span>
|
||||
<div>
|
||||
<strong>Vérifier la santé du système</strong>
|
||||
<p>Statistiques → Consulter les métriques → Vérifier le traitement → Surveiller le stockage</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-card">
|
||||
<span class="action-icon">⚙️</span>
|
||||
<div>
|
||||
<strong>Modifier les paramètres</strong>
|
||||
<p>Paramètres du système → Ajuster les limites → Configurer les délais → Enregistrer</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-card">
|
||||
<span class="action-icon">🔄</span>
|
||||
<div>
|
||||
<strong>Vérifier l'indexation sémantique</strong>
|
||||
<p>Recherche sémantique → Vérifier le statut → Traiter les éléments en attente → Suivre la progression</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
## Besoin d'aide?
|
||||
|
||||
<div class="help-section">
|
||||
<div class="help-item">
|
||||
<span class="help-icon">📖</span>
|
||||
<span>Consultez le <a href="../depannage">Guide de dépannage</a></span>
|
||||
</div>
|
||||
<div class="help-item">
|
||||
<span class="help-icon">📧</span>
|
||||
<span>Contactez le support InnovA AI pour toute question technique</span>
|
||||
</div>
|
||||
<div class="help-item">
|
||||
<span class="help-icon">💾</span>
|
||||
<span>InnovA AI effectue des sauvegardes régulières de vos données</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
Prêt à administrer votre instance DictIA? Commencez par la [Gestion des utilisateurs](gestion-utilisateurs.md) →
|
||||
62
client_docs/guide-admin/modeles-ia.md
Normal file
62
client_docs/guide-admin/modeles-ia.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Modèles d'IA
|
||||
|
||||
DictIA utilise des modèles d'intelligence artificielle pour plusieurs fonctionnalités clés de la plateforme. Cette section explique à quoi servent ces modèles et ce qui peut être ajusté.
|
||||
|
||||
## Fonctionnalités alimentées par l'IA
|
||||
|
||||
Les modèles d'IA de DictIA sont utilisés pour :
|
||||
|
||||
- **Génération de résumés** : Création de résumés intelligents de vos transcriptions
|
||||
- **Génération de titres** : Attribution automatique de titres descriptifs aux enregistrements
|
||||
- **Extraction d'événements** : Identification d'événements dignes d'être ajoutés au calendrier
|
||||
- **Conversation IA** : Réponses aux questions sur vos enregistrements
|
||||
- **Identification des locuteurs** : Détection des noms des locuteurs à partir du contexte de la conversation
|
||||
|
||||
## Configuration des modèles
|
||||
|
||||
La configuration technique des modèles d'IA (choix du fournisseur, clés d'API, paramètres du modèle) est gérée par l'équipe InnovA AI. Contactez le support InnovA AI pour ajuster ces paramètres.
|
||||
|
||||
### Ce que vous pouvez contrôler via l'interface
|
||||
|
||||
Bien que la configuration technique soit gérée par InnovA AI, vous pouvez influencer le comportement de l'IA via :
|
||||
|
||||
- **[Consignes par défaut](prompts.md)** : Personnalisez les instructions envoyées à l'IA pour la génération de résumés. C'est le moyen le plus direct d'adapter l'IA à vos besoins.
|
||||
- **[Limite de longueur de transcription](parametres-systeme.md#limite-de-longueur-de-transcription)** : Contrôlez la quantité de texte envoyée à l'IA, ce qui affecte la qualité et les coûts.
|
||||
- **[Budgets de jetons par utilisateur](gestion-utilisateurs.md#gestion-du-budget-de-jetons-ia)** : Définissez des limites mensuelles de consommation d'IA pour chaque utilisateur.
|
||||
|
||||
### Modèles disponibles
|
||||
|
||||
DictIA peut être configuré avec différents modèles d'IA selon vos besoins :
|
||||
|
||||
- **Modèles pour les résumés** : Des modèles de différentes capacités sont disponibles, du plus économique au plus performant
|
||||
- **Modèle distinct pour la conversation IA** : Un modèle séparé peut être configuré spécifiquement pour les interactions en temps réel, permettant d'optimiser le rapport qualité/coût
|
||||
|
||||
### Optimisation des coûts
|
||||
|
||||
Pour réduire les coûts tout en maintenant la qualité :
|
||||
|
||||
1. **Ajustez les consignes** : Des consignes bien rédigées produisent de meilleurs résultats avec moins de jetons
|
||||
2. **Définissez des budgets** : Utilisez les budgets de jetons pour contrôler la consommation par utilisateur
|
||||
3. **Ajustez la limite de transcription** : Limitez la quantité de texte envoyée à l'IA pour les longs enregistrements
|
||||
|
||||
Pour des ajustements plus avancés (changement de modèle, modification des paramètres techniques), contactez le support InnovA AI.
|
||||
|
||||
### Qualité des résumés
|
||||
|
||||
Si la qualité des résumés ne répond pas à vos attentes, voici les actions que vous pouvez entreprendre :
|
||||
|
||||
1. **Révisez vos [consignes par défaut](prompts.md)** : C'est souvent la solution la plus efficace
|
||||
2. **Vérifiez la limite de transcription** : Une limite trop basse peut empêcher l'IA d'avoir suffisamment de contexte
|
||||
3. **Contactez le support InnovA AI** : L'équipe peut recommander un modèle plus performant ou ajuster les paramètres techniques
|
||||
|
||||
### Coûts élevés
|
||||
|
||||
Si vous observez des coûts d'IA plus élevés que prévu :
|
||||
|
||||
1. Vérifiez les [statistiques d'utilisation des jetons](statistiques.md#statistiques-dutilisation-des-jetons) pour identifier les gros consommateurs
|
||||
2. Ajustez les [budgets de jetons](gestion-utilisateurs.md#gestion-du-budget-de-jetons-ia) pour les utilisateurs concernés
|
||||
3. Contactez le support InnovA AI pour explorer les options de modèles plus économiques
|
||||
|
||||
---
|
||||
|
||||
Suivant : [Consignes par défaut](prompts.md) | Retour au [Guide d'administration](index.md)
|
||||
115
client_docs/guide-admin/parametres-systeme.md
Normal file
115
client_docs/guide-admin/parametres-systeme.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# Paramètres du système
|
||||
|
||||
Les paramètres du système permettent de configurer les comportements fondamentaux qui affectent chaque utilisateur et chaque enregistrement de votre instance DictIA. Ces paramètres globaux façonnent le fonctionnement du système, des limites techniques aux fonctionnalités visibles par les utilisateurs.
|
||||
|
||||
## Limite de longueur de transcription
|
||||
|
||||
La limite de longueur de transcription détermine la quantité de texte envoyée à l'IA lors de la génération de résumés ou de la conversation IA. Ce paramètre a un effet important sur la qualité et les coûts.
|
||||
|
||||
- **Sans limite** : La transcription complète est envoyée à l'IA, peu importe la longueur. Cela garantit un contexte complet, mais peut devenir coûteux pour les longs enregistrements. Cette limite s'applique également à la détection automatique des locuteurs.
|
||||
- **Avec limite** (ex. : 50 000 caractères) : Crée un plafond sur la consommation d'API. Les transcriptions très longues seront tronquées, n'envoyant que le début à l'IA.
|
||||
|
||||
Pour les réunions typiques de moins d'une heure, 50 000 caractères capturent généralement tout. Pour les sessions plus longues, vous pouvez augmenter cette limite ou encourager les utilisateurs à diviser leurs enregistrements.
|
||||
|
||||
## Taille maximale des fichiers
|
||||
|
||||
La taille maximale protège votre système contre les téléversements trop volumineux tout en permettant aux utilisateurs de travailler avec des enregistrements raisonnables. La valeur par défaut de 300 Mo accommode plusieurs heures d'audio compressé.
|
||||
|
||||
Augmenter cette limite permet des enregistrements plus longs mais nécessite une considération attentive. Les fichiers plus gros prennent plus de temps à téléverser, consomment plus de stockage et pourraient expirer durant le traitement.
|
||||
|
||||
Si les utilisateurs atteignent fréquemment la limite, demandez-vous s'ils ont réellement besoin d'enregistrements aussi longs. Souvent, diviser les longues sessions en segments logiques produit de meilleurs résultats.
|
||||
|
||||
## Délai d'attente de la transcription
|
||||
|
||||
Le délai d'attente détermine combien de temps DictIA attendra que le service de transcription complète son travail. La valeur par défaut de 1 800 secondes (30 minutes) convient à la plupart des enregistrements.
|
||||
|
||||
- **Trop court** : Les enregistrements plus longs échouent même si le service fonctionne normalement
|
||||
- **Trop long** : Les ressources système restent mobilisées inutilement en cas d'échec réel
|
||||
|
||||
Ajustez ce paramètre en fonction de vos enregistrements les plus longs typiques. Pour les enregistrements de plusieurs heures, une valeur de 3 600 secondes ou plus peut être nécessaire. Contactez le support InnovA AI pour ajuster ce paramètre si nécessaire.
|
||||
|
||||
## Avis d'enregistrement
|
||||
|
||||
L'avis d'enregistrement s'affiche avant que les utilisateurs ne démarrent une session d'enregistrement. Idéal pour les mentions légales, les rappels de politiques ou les directives d'utilisation. Ce message en format Markdown garantit que les utilisateurs comprennent leurs responsabilités avant de créer du contenu.
|
||||
|
||||
Les organisations utilisent souvent cet avis pour les exigences de conformité, notamment la Loi 25 au Québec :
|
||||
|
||||
- Rappeler les exigences de consentement
|
||||
- Référencer les politiques de gestion des données
|
||||
- Mentionner les directives d'utilisation appropriée
|
||||
|
||||
!!! info "Support complet du Markdown"
|
||||
L'avis d'enregistrement supporte le formatage Markdown complet : titres, listes, gras, italique, liens, blocs de code et citations.
|
||||
|
||||
Exemple :
|
||||
```markdown
|
||||
## Consentement requis
|
||||
|
||||
En démarrant cet enregistrement, vous acceptez de :
|
||||
|
||||
1. Obtenir le consentement de tous les participants
|
||||
2. Respecter la [politique de confidentialité](https://exemple.com/confidentialite)
|
||||
3. Traiter les enregistrements conformément à la **Loi 25**
|
||||
|
||||
> **Important** : Les enregistrements contenant des renseignements personnels
|
||||
> doivent être gérés selon la politique de rétention en vigueur.
|
||||
```
|
||||
|
||||
Gardez les avis concis et pertinents. Les utilisateurs voient ce message fréquemment; un texte trop long sera simplement ignoré.
|
||||
|
||||
## Avis de téléversement
|
||||
|
||||
L'avis de téléversement fonctionne comme l'avis d'enregistrement, mais s'affiche lorsque les utilisateurs téléversent des fichiers. Chaque fois qu'un utilisateur glisse-dépose des fichiers ou les sélectionne pour téléversement, il voit cet avis et doit l'accepter.
|
||||
|
||||
Utile lorsque les fichiers téléversés peuvent contenir du contenu de tiers ou lorsque votre organisation doit rappeler les politiques de gestion des données. Supporte le formatage Markdown complet.
|
||||
|
||||
Laissez ce champ vide pour désactiver complètement l'avis de téléversement.
|
||||
|
||||
## Bannière personnalisée
|
||||
|
||||
La bannière personnalisée affiche un message persistant en haut de la zone de contenu principal pour tous les utilisateurs. Utile pour les annonces, les avis de maintenance ou les rappels de conformité.
|
||||
|
||||
La bannière apparaît sous l'en-tête et au-dessus du contenu principal. Les utilisateurs peuvent la fermer en cliquant sur le X, mais elle réapparaît au rafraîchissement de la page.
|
||||
|
||||
Exemples d'utilisation :
|
||||
|
||||
```markdown
|
||||
**Mise à jour système** — DictIA sera brièvement indisponible dimanche de 22h à minuit pour maintenance.
|
||||
```
|
||||
|
||||
```markdown
|
||||
Tous les enregistrements sont assujettis à notre [politique d'utilisation](https://exemple.com/politique). Contactez le support pour toute question.
|
||||
```
|
||||
|
||||
Laissez ce champ vide pour masquer la bannière.
|
||||
|
||||
## Impact à l'échelle du système
|
||||
|
||||
Chaque paramètre de cette page affecte tous les utilisateurs immédiatement. Les modifications prennent effet dès l'enregistrement, sans nécessiter de redémarrage. Cette application immédiate signifie que vous devriez tester les changements avec soin et communiquer les modifications importantes à vos utilisateurs.
|
||||
|
||||
Le bouton de rafraîchissement recharge les paramètres, utile si plusieurs administrateurs effectuent des changements simultanément.
|
||||
|
||||
## Paramètres supplémentaires
|
||||
|
||||
Certains paramètres du système ne sont pas accessibles via l'interface web et nécessitent une intervention technique. Ces paramètres incluent notamment :
|
||||
|
||||
- **Collaboration et partage** : Activation du partage interne, du partage public et de la visibilité des noms d'utilisateur
|
||||
- **Permissions utilisateur** : Contrôle de la capacité des utilisateurs à supprimer leurs propres enregistrements
|
||||
- **Rétention et suppression automatique** : Voir la section [Rétention](retention.md) pour plus de détails
|
||||
- **Dossiers** : Activation de la fonctionnalité de dossiers pour organiser les enregistrements
|
||||
- **Compression audio** : Configuration de la compression automatique des fichiers audio
|
||||
- **Conservation vidéo** : Option de conserver les fichiers vidéo pour la lecture dans le navigateur
|
||||
|
||||
Contactez le support InnovA AI pour ajuster ces paramètres.
|
||||
|
||||
## Dépannage
|
||||
|
||||
Lorsque les enregistrements échouent de manière constante, vérifiez s'ils dépassent vos limites configurées. Les utilisateurs ne réalisent pas toujours que leurs enregistrements dépassent les limites, surtout lorsqu'ils téléversent du contenu existant.
|
||||
|
||||
Si les coûts d'API augmentent de façon inattendue, vérifiez votre limite de longueur de transcription. Un seul utilisateur téléversant de nombreux longs enregistrements pourrait augmenter considérablement la consommation si aucune limite n'est définie.
|
||||
|
||||
Pour tout problème technique, contactez le support InnovA AI.
|
||||
|
||||
---
|
||||
|
||||
Suivant : [Modèles d'IA](modeles-ia.md) →
|
||||
121
client_docs/guide-admin/prompts.md
Normal file
121
client_docs/guide-admin/prompts.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# Consignes par défaut
|
||||
|
||||
L'onglet Consignes par défaut vous permet de façonner la manière dont l'IA interprète et résume les enregistrements dans l'ensemble de votre instance DictIA. C'est ici que vous établissez l'intelligence de base que les utilisateurs expérimentent lorsqu'ils n'ont pas personnalisé leurs propres consignes.
|
||||
|
||||
## Comprendre la hiérarchie des consignes
|
||||
|
||||
DictIA utilise une hiérarchie pour déterminer quelle consigne utiliser pour chaque enregistrement. Ce système offre de la flexibilité tout en maintenant le contrôle.
|
||||
|
||||
1. **Consignes par étiquette** (priorité la plus haute) : Lorsqu'un enregistrement possède des étiquettes avec des consignes associées, celles-ci ont la priorité absolue. Plusieurs consignes d'étiquettes se combinent intelligemment, permettant des configurations sophistiquées pour des types de contenu spécialisés.
|
||||
|
||||
2. **Consigne personnelle de l'utilisateur** : Définie dans les paramètres du compte de l'utilisateur. Permet aux individus d'adapter les résumés à leurs besoins spécifiques sans affecter les autres.
|
||||
|
||||
3. **Consigne par défaut de l'administrateur** : Configurée sur cette page, elle sert de fondation pour la majorité des résumés. C'est ce que les nouveaux utilisateurs expérimentent et ce sur quoi les utilisateurs à long terme comptent.
|
||||
|
||||
4. **Consigne système de secours** : Si tout le reste échoue, une consigne intégrée garantit que des résumés sont toujours générés.
|
||||
|
||||
## Rédiger des consignes efficaces
|
||||
|
||||
Votre consigne par défaut est plus qu'une instruction technique : c'est un modèle de compréhension. Considérez votre base d'utilisateurs lors de la conception :
|
||||
|
||||
- Un cabinet juridique pourrait se concentrer sur les détails de dossiers et les précédents
|
||||
- Une institution de recherche pourrait mettre l'accent sur les méthodologies et les résultats
|
||||
- Une agence créative pourrait mettre en valeur les concepts et les commentaires des clients
|
||||
|
||||
### Conseils de rédaction
|
||||
|
||||
- **Structurez clairement** : Utilisez des listes à puces ou des sections numérotées pour organiser la sortie
|
||||
- **Spécifiez le niveau de détail** : "Aperçu bref" versus "Analyse complète" produisent des résultats très différents
|
||||
- **Concevez pour la polyvalence** : Cette consigne s'applique à tout, des vérifications de 5 minutes aux ateliers de 2 heures
|
||||
- **Évitez les exigences trop spécifiques** : Pas tous les enregistrements contiennent des "décisions" ou des "éléments d'action"
|
||||
- **Utilisez un langage conditionnel** : Des phrases comme "si applicable" ou "lorsque discuté" permettent à l'IA de sauter les sections non pertinentes
|
||||
|
||||
## L'éditeur de consignes
|
||||
|
||||
La zone de texte affiche votre consigne actuelle avec le support complet du Markdown. Les modifications sont enregistrées immédiatement lorsque vous cliquez sur **Enregistrer les modifications**. Il n'y a pas de brouillon ni de mise en attente : les modifications affectent tous les nouveaux résumés instantanément.
|
||||
|
||||
Les utilisateurs peuvent régénérer les résumés pour appliquer les consignes mises à jour aux enregistrements existants.
|
||||
|
||||
Le bouton **Réinitialiser par défaut** offre un filet de sécurité, permettant de revenir à la consigne originale si vos personnalisations ne produisent pas les résultats attendus.
|
||||
|
||||
L'horodatage indique la dernière modification, utile pour coordonner les changements entre plusieurs administrateurs.
|
||||
|
||||
## Voir la structure complète de la consigne IA
|
||||
|
||||
La section dépliable **Voir la structure complète de la consigne IA** révèle comment votre consigne s'intègre dans l'instruction complète envoyée à l'IA. Cette vue technique montre la consigne système, votre consigne personnalisée et l'intégration de la transcription.
|
||||
|
||||
Comprendre cette structure vous aide à :
|
||||
|
||||
- Éviter de répéter des instructions déjà gérées par la consigne système
|
||||
- Comprendre pourquoi certaines formulations fonctionnent mieux
|
||||
- Diagnostiquer les problèmes lorsque les résumés ne répondent pas aux attentes
|
||||
|
||||
## Cas d'utilisation créatifs avec les étiquettes
|
||||
|
||||
Les étiquettes avec des consignes personnalisées permettent des transformations puissantes du contenu :
|
||||
|
||||
### Enregistrements de recettes
|
||||
|
||||
Créez une étiquette "Recette" avec une consigne comme : "Convertis cette narration culinaire en une recette formatée avec la liste des ingrédients, les étapes, les temps de cuisson et le nombre de portions."
|
||||
|
||||
L'IA transforme un flux de conscience désordonné en une recette propre et utilisable.
|
||||
|
||||
### Notes de cours
|
||||
|
||||
Une étiquette "Cours" pourrait utiliser : "Extrais les concepts principaux, les exemples, la terminologie clé avec définitions et les applications pratiques mentionnées. Organise en format plan pour des notes d'étude."
|
||||
|
||||
### Éléments d'action de réunion
|
||||
|
||||
Créez une étiquette "Réunion de projet" avec : "Concentre-toi exclusivement sur les éléments d'action, les décisions prises et les prochaines étapes. Pour chaque élément d'action, identifie le responsable et les échéances mentionnées."
|
||||
|
||||
### Sessions de remue-méninges
|
||||
|
||||
Une étiquette "Idées" avec : "Liste chaque idée distincte mentionnée, peu importe la brièveté de la discussion. Pour chaque idée, note les réactions ou préoccupations immédiates."
|
||||
|
||||
### Revues de code
|
||||
|
||||
Étiquette "Revue de code" : "Pour chaque morceau de code ou système discuté, liste : 1) Ce qui a été revu, 2) Problèmes identifiés, 3) Changements suggérés, 4) Qui implémentera les correctifs."
|
||||
|
||||
## Combinaison d'étiquettes et ordre
|
||||
|
||||
Lorsqu'un enregistrement possède plusieurs étiquettes avec des consignes, elles se combinent dans l'ordre d'application. Exemples :
|
||||
|
||||
### Cours personnel + cours spécifique
|
||||
|
||||
- Étiquette "Mes cours" : "Organise comme notes d'étude avec des en-têtes clairs."
|
||||
- Étiquette "Biologie 301" : "Porte une attention particulière aux processus biologiques et à la terminologie."
|
||||
- **Résultat** : Des notes d'étude formatées avec un focus sur le contenu biologique.
|
||||
|
||||
### Réunion client + revue juridique
|
||||
|
||||
- Étiquette "Réunion client" : "Extrais les exigences, préoccupations et préférences du client."
|
||||
- Étiquette "Revue juridique" : "Identifie les considérations légales et les facteurs de risque mentionnés."
|
||||
- **Résultat** : Les besoins du client plus les implications juridiques dans un seul résumé.
|
||||
|
||||
### Ordre des étiquettes
|
||||
|
||||
Les consignes plus spécifiques devraient généralement venir en dernier, car elles raffinent la sortie des consignes générales. Commencez large (type de format) puis ajoutez les spécificités (domaines de focus).
|
||||
|
||||
Testez vos combinaisons d'étiquettes avec des enregistrements de référence pour voir quel ordre produit les résultats souhaités.
|
||||
|
||||
## Coordonner avec les consignes utilisateur
|
||||
|
||||
Votre consigne par défaut devrait compléter, et non rivaliser avec, la personnalisation des utilisateurs. Concevez-la comme une base solide qui fonctionne pour la plupart des cas, tout en encourageant les utilisateurs avancés à personnaliser selon leurs besoins.
|
||||
|
||||
Communiquez votre stratégie de consignes aux utilisateurs pour qu'ils puissent décider si la personnalisation leur serait bénéfique. Si certains départements ont besoin de résumés spécialisés, fournissez des consignes recommandées qu'ils peuvent utiliser.
|
||||
|
||||
## Mesurer l'efficacité des consignes
|
||||
|
||||
- **Fréquence des modifications** : Si les utilisateurs modifient souvent les résumés générés, votre consigne pourrait avoir besoin d'ajustement
|
||||
- **Échantillonnage périodique** : Révisez un échantillon de résumés régulièrement pour évaluer la qualité
|
||||
- **Rétroaction des utilisateurs** : Sollicitez des commentaires sur la qualité des résumés, en particulier auprès des utilisateurs qui n'ont pas personnalisé leurs consignes
|
||||
|
||||
## Pièges courants à éviter
|
||||
|
||||
- **Consignes trop restrictives** : Pas tous les enregistrements contiennent des "décisions" ou des "éléments d'action". Forcer l'IA à trouver ces éléments quand ils n'existent pas produit du contenu de remplissage sans valeur.
|
||||
- **Demandes impossibles** : Demander les "préoccupations non exprimées" ou "ce qui n'a pas été discuté" dépasse les capacités de l'analyse de transcription. L'IA ne peut travailler qu'avec ce qui a été réellement dit.
|
||||
- **Consignes trop longues** : Chaque instruction ajoute de la complexité. Concentrez-vous sur l'essentiel plutôt que d'essayer de capturer chaque détail possible.
|
||||
|
||||
---
|
||||
|
||||
Suivant : [Recherche sémantique](recherche-semantique.md) →
|
||||
60
client_docs/guide-admin/recherche-semantique.md
Normal file
60
client_docs/guide-admin/recherche-semantique.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# Recherche sémantique
|
||||
|
||||
La section Recherche sémantique contrôle l'intelligence derrière la Recherche IA, la capacité de recherche sémantique de DictIA qui permet aux utilisateurs de trouver de l'information dans tous leurs enregistrements en posant des questions en langage naturel.
|
||||
|
||||
## Comprendre la Recherche IA
|
||||
|
||||
La Recherche IA fonctionne en plusieurs étapes :
|
||||
|
||||
1. Chaque transcription est découpée en segments de texte qui se chevauchent
|
||||
2. Ces segments sont convertis en représentations mathématiques (indexation sémantique)
|
||||
3. Les segments indexés sont stockés dans un format permettant la recherche
|
||||
4. Lorsqu'un utilisateur pose une question, celle-ci est convertie dans le même format mathématique et comparée à tous les segments stockés
|
||||
|
||||
Cette approche va bien au-delà de la simple correspondance de mots-clés. Le système comprend que "préoccupations budgétaires" est lié à "contraintes financières" et "dépassements de coûts", même si les mots exacts diffèrent. Cette compréhension sémantique rend la Recherche IA très puissante pour découvrir de l'information que les utilisateurs ne se rappellent pas précisément.
|
||||
|
||||
## Statut du traitement
|
||||
|
||||
Les cartes de statut donnent un aperçu immédiat de la santé de votre recherche sémantique :
|
||||
|
||||
- **Enregistrements totaux** : Le nombre de fichiers audio dans votre système
|
||||
- **Traités pour la Recherche IA** : Le nombre d'enregistrements convertis en segments indexés et recherchables. Ce nombre devrait éventuellement correspondre au total, avec un léger décalage pendant le traitement
|
||||
- **En attente de traitement** : Les enregistrements en attente d'indexation sémantique. Ce nombre augmente lorsque les utilisateurs téléversent du nouveau contenu et diminue à mesure que le traitement avance
|
||||
- **Segments totaux** : Les morceaux détaillés dans lesquels vos enregistrements ont été divisés. Un enregistrement typique d'une heure génère environ 50 à 60 segments
|
||||
- **Statut de l'indexation** : Un indicateur rapide de santé. "Disponible" en vert signifie que tout fonctionne correctement
|
||||
|
||||
## Progression du traitement
|
||||
|
||||
La barre de progression montre l'avancement en temps réel dans la file d'attente d'indexation sémantique :
|
||||
|
||||
- **100 %** : Tous les enregistrements sont traités et recherchables
|
||||
- **Pourcentages inférieurs** : Le traitement est en cours
|
||||
- **Barre bloquée** : Le traitement s'est peut-être arrêté. Contactez le support InnovA AI
|
||||
|
||||
## Gérer la file d'attente
|
||||
|
||||
Le bouton **Actualiser le statut** met à jour toutes les statistiques et indicateurs de progression. L'interface ne se rafraîchit pas automatiquement; des actualisations manuelles garantissent que vous voyez les informations actuelles.
|
||||
|
||||
Si le système indique que des enregistrements nécessitent un traitement mais que la progression n'avance pas, contactez le support InnovA AI pour diagnostiquer le problème.
|
||||
|
||||
Le système de traitement est conçu pour être résilient. Si le traitement échoue pour un enregistrement spécifique, le système le marque et passe au suivant plutôt que de rester bloqué.
|
||||
|
||||
## Conseils pour de meilleurs résultats de recherche
|
||||
|
||||
La qualité des résultats de recherche dépend en partie de la formulation des requêtes. Encouragez les utilisateurs à :
|
||||
|
||||
- **Poser des questions complètes** plutôt que de taper des mots-clés. "Qu'est-ce que Marie a dit au sujet du budget?" fonctionne mieux que simplement "Marie budget".
|
||||
- **Utiliser des termes variés** : La recherche sémantique comprend les synonymes et les concepts connexes
|
||||
- **Être spécifique** lorsque c'est possible : Plus la question est précise, plus les résultats seront pertinents
|
||||
|
||||
## Considérations de performance
|
||||
|
||||
La recherche sémantique croît de façon prévisible avec votre contenu. Chaque segment nécessite environ 2 Ko de stockage. Un enregistrement typique d'une heure nécessite environ 100 Ko de stockage d'indexation. Le système maintient des performances rapides même avec de grandes quantités de données.
|
||||
|
||||
Si votre instance croît de façon significative, les politiques de [rétention](retention.md) aident à gérer à la fois le stockage et la performance de la recherche. La recherche sémantique n'inclut que les enregistrements actifs; la suppression du contenu obsolète améliore les performances de recherche.
|
||||
|
||||
Pour toute question technique sur la performance de la recherche sémantique, contactez le support InnovA AI.
|
||||
|
||||
---
|
||||
|
||||
Retour au [Guide d'administration](index.md) →
|
||||
212
client_docs/guide-admin/retention.md
Normal file
212
client_docs/guide-admin/retention.md
Normal file
@@ -0,0 +1,212 @@
|
||||
# Rétention et suppression automatique
|
||||
|
||||
Le système de suppression automatique assure une gestion du cycle de vie de vos enregistrements, en conformité avec vos politiques de rétention des données et les exigences de la Loi 25 du Québec.
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
Le système de rétention automatisée vous aide à :
|
||||
|
||||
- **Respecter les politiques de rétention** : Suppression automatique des enregistrements après une période définie, conformément à la Loi 25
|
||||
- **Gérer le stockage** : Prévenir la croissance illimitée des fichiers audio
|
||||
- **Préserver les données essentielles** : Conserver les transcriptions et métadonnées même après la suppression de l'audio
|
||||
- **Protéger les enregistrements importants** : Exempter des enregistrements spécifiques de la suppression automatique
|
||||
|
||||
## Modes de suppression
|
||||
|
||||
DictIA offre deux modes de suppression :
|
||||
|
||||
### Mode audio seulement
|
||||
|
||||
- **Supprime** : Le fichier audio uniquement
|
||||
- **Conserve** : La transcription, le résumé, les notes et les métadonnées
|
||||
- **Cas d'utilisation** : Conservation à long terme avec optimisation du stockage
|
||||
- **Résultat** : Les enregistrements apparaissent en mode "Archivé", la transcription reste recherchable
|
||||
|
||||
### Mode suppression complète
|
||||
|
||||
- **Supprime** : L'enregistrement complet, incluant l'audio, la transcription, le résumé et les notes
|
||||
- **Conserve** : Rien - l'enregistrement est définitivement supprimé
|
||||
- **Cas d'utilisation** : Suppression complète des données pour la conformité à la Loi 25
|
||||
- **Résultat** : L'enregistrement est entièrement retiré du système
|
||||
|
||||
## Système de rétention multiniveau
|
||||
|
||||
DictIA utilise un système hiérarchique de politiques de rétention :
|
||||
|
||||
### 1. Rétention globale (à l'échelle du système)
|
||||
|
||||
La période de rétention globale s'applique à tous les enregistrements, sauf si elle est remplacée par une étiquette spécifique. Contactez le support InnovA AI pour ajuster ce paramètre.
|
||||
|
||||
### 2. Rétention par étiquette
|
||||
|
||||
Les étiquettes peuvent remplacer la période de rétention globale avec des périodes personnalisées. Cela est particulièrement puissant avec les étiquettes de groupe.
|
||||
|
||||
| Type de contenu | Rétention | Justification |
|
||||
|----------------|-----------|---------------|
|
||||
| Enregistrements sans étiquette | Globale (ex. : 90 jours) | Valeur par défaut du système |
|
||||
| Archives légales | 2555 jours (7 ans) | Conformité juridique |
|
||||
| Réunions quotidiennes | 14 jours | Contenu éphémère |
|
||||
|
||||
Lorsqu'un enregistrement possède plusieurs étiquettes avec différentes périodes de rétention, la période la **plus courte** s'applique. Cela garantit la conformité avec la politique la plus restrictive.
|
||||
|
||||
### 3. Protection par étiquette
|
||||
|
||||
Les étiquettes individuelles peuvent protéger les enregistrements contre la suppression automatique de manière permanente.
|
||||
|
||||
**Hiérarchie d'exemple** :
|
||||
|
||||
- Rétention globale : 90 jours
|
||||
- Étiquette "Revues de sprint" : 180 jours (plus long que la globale)
|
||||
- Étiquette "Réunions quotidiennes" : 14 jours (plus court que la globale)
|
||||
- Étiquette "Juridique" avec protection activée : Jamais supprimé (permanent)
|
||||
|
||||
## Protéger des enregistrements
|
||||
|
||||
Pour protéger des enregistrements contre la suppression automatique :
|
||||
|
||||
1. Accédez à **Paramètres du compte** → onglet **Étiquettes**
|
||||
2. Cliquez sur **Créer une étiquette** ou **Modifier** une étiquette existante
|
||||
3. Activez la case **Protéger contre la suppression automatique**
|
||||
4. Appliquez cette étiquette aux enregistrements à protéger
|
||||
|
||||
Les enregistrements avec des étiquettes protégées sont exemptés de la suppression automatique, peu importe leur âge ou la période de rétention.
|
||||
|
||||
## Enregistrements archivés
|
||||
|
||||
Lorsque le mode de suppression est "audio seulement", les enregistrements deviennent "archivés" après la suppression de l'audio.
|
||||
|
||||
### Accéder aux enregistrements archivés
|
||||
|
||||
1. Ouvrez la barre latérale **Enregistrements**
|
||||
2. Cliquez sur **Filtres avancés**
|
||||
3. Activez l'option **Enregistrements archivés**
|
||||
|
||||
### Fonctionnalités disponibles pour les enregistrements archivés
|
||||
|
||||
| Fonctionnalité | Disponible | Notes |
|
||||
|----------------|------------|-------|
|
||||
| Lire la transcription | Oui | Transcription complète accessible |
|
||||
| Rechercher le contenu | Oui | La recherche textuelle fonctionne toujours |
|
||||
| Lire le résumé | Oui | Le résumé IA est préservé |
|
||||
| Voir/modifier les notes | Oui | Toutes les métadonnées accessibles |
|
||||
| Écouter l'audio | Non | Le fichier audio a été supprimé |
|
||||
| Retraiter | Non | L'audio source n'est plus disponible |
|
||||
| Partager | Oui | La transcription peut être partagée |
|
||||
| Exporter | Oui | Téléchargement de la transcription, du résumé et des notes |
|
||||
|
||||
### Indicateurs d'archivage
|
||||
|
||||
- **Barre latérale** : Badge gris "Archivé" à côté du titre de l'enregistrement
|
||||
- **Lecteur** : Bannière informative indiquant que l'audio a été supprimé mais que la transcription reste disponible
|
||||
- **Filtre** : Option de vue "Archivé" dans les filtres avancés
|
||||
|
||||
## Nettoyage des profils de locuteurs
|
||||
|
||||
Par défaut, les profils de locuteurs et les empreintes vocales sont préservés même lorsque tous les enregistrements associés sont supprimés. Cela est dû au fait que les empreintes vocales sont des données agrégées qui ne peuvent pas être reconstruites à partir d'enregistrements individuels.
|
||||
|
||||
Pour les déploiements qui doivent traiter les empreintes vocales comme des données biométriques (conformité Loi 25), le nettoyage automatique peut être activé. Contactez le support InnovA AI pour ajuster ce paramètre.
|
||||
|
||||
### Conformité Loi 25 et protection de la vie privée
|
||||
|
||||
Lorsque le nettoyage automatique est activé :
|
||||
|
||||
- **Minimisation des données** : Les données vocales sont supprimées lorsqu'elles ne sont plus nécessaires
|
||||
- **Droit à l'effacement** : Les profils vocaux sont supprimés lorsque les enregistrements sont retirés
|
||||
- **Transparence** : L'activité de nettoyage est journalisée à des fins d'audit
|
||||
- **Automatisation** : Aucune intervention manuelle n'est requise lorsque combiné aux politiques de rétention
|
||||
|
||||
## Cas d'utilisation pratiques
|
||||
|
||||
### Usage personnel
|
||||
|
||||
Vous enregistrez tout pendant votre journée de travail. La plupart de ces enregistrements sont éphémères. Configurez une rétention globale de 30 jours avec suppression audio seulement. Après un mois, les fichiers audio disparaissent mais les transcriptions recherchables demeurent. Si quelque chose s'avère important, étiquetez-le avec une étiquette protégée avant l'expiration.
|
||||
|
||||
### Collaboration de groupe
|
||||
|
||||
| Type de contenu | Approche de rétention | Raison |
|
||||
|----------------|----------------------|--------|
|
||||
| Réunions quotidiennes | Étiquette avec rétention de 14 jours | Mises à jour de routine, aucune valeur à long terme |
|
||||
| Planification de sprint | Étiquette avec rétention de 90 jours | Valeur de référence pour le trimestre en cours |
|
||||
| Décisions d'architecture | Étiquette avec protection activée | Documenter les choix importants de façon permanente |
|
||||
| Appels clients (ventes) | Étiquette avec rétention de 1 an | Durée du cycle de vente + suivi |
|
||||
| Entrevues (RH) | Étiquette avec rétention de 2 ans | Délai typique de litige en emploi |
|
||||
| Réunions juridiques | Étiquette protégée | Rétention indéfinie pour conformité |
|
||||
|
||||
Chaque groupe configure ses étiquettes une seule fois. Les membres n'ont qu'à étiqueter normalement leurs enregistrements et la gestion du cycle de vie se fait automatiquement.
|
||||
|
||||
### Exigences de conformité (Loi 25)
|
||||
|
||||
La Loi 25 du Québec exige que les organisations gèrent adéquatement les renseignements personnels, incluant leur durée de conservation. Le système de rétention de DictIA vous aide à :
|
||||
|
||||
- Établir des périodes de rétention conformes à vos obligations légales
|
||||
- Automatiser la suppression des données lorsque la période de conservation est expirée
|
||||
- Protéger les enregistrements qui doivent être conservés pour des raisons légales
|
||||
- Documenter vos politiques de rétention de manière transparente
|
||||
|
||||
### Gestion des coûts de stockage
|
||||
|
||||
Les fichiers audio sont volumineux (une réunion d'une heure peut représenter 50 à 100 Mo). Les transcriptions sont légères (10 à 20 Ko pour la même réunion). Le mode suppression audio seulement conserve le texte recherchable tout en libérant le stockage.
|
||||
|
||||
Avec une rétention de 90 jours en mode audio seulement, les enregistrements de plus de 90 jours perdent leur audio mais restent entièrement recherchables via la Recherche IA. Vous pouvez lire les transcriptions, consulter les résumés et voir les notes, mais vous ne pouvez plus écouter l'audio original.
|
||||
|
||||
## Bonnes pratiques
|
||||
|
||||
### Pour la conformité Loi 25
|
||||
|
||||
1. Définissez des périodes de rétention appropriées à votre secteur d'activité
|
||||
2. Utilisez des étiquettes protégées pour les documents nécessitant une rétention indéfinie (ex. : "Rétention légale", "Permanent")
|
||||
3. Documentez votre politique de rétention dans la documentation de conformité de votre organisation
|
||||
4. Activez le nettoyage des profils de locuteurs si les empreintes vocales constituent des renseignements personnels dans votre contexte
|
||||
|
||||
### Pour la gestion du stockage
|
||||
|
||||
1. Commencez avec le mode suppression audio seulement pour préserver les transcriptions recherchables
|
||||
2. Utilisez des périodes plus courtes pour le contenu de routine
|
||||
3. Protégez le contenu important avec des étiquettes dédiées
|
||||
|
||||
### Pour les groupes
|
||||
|
||||
1. Établissez une rétention globale conservatrice comme base
|
||||
2. Configurez des étiquettes de groupe avec des rétentions personnalisées selon les besoins
|
||||
3. Utilisez des étiquettes protégées pour le contenu nécessitant une conservation permanente
|
||||
4. Documentez les politiques de rétention pour que les membres du groupe comprennent les attentes
|
||||
|
||||
## Processus de suppression
|
||||
|
||||
Le processus de suppression automatique fonctionne ainsi :
|
||||
|
||||
1. **Vérification automatique** (quotidienne)
|
||||
2. **Identification** des enregistrements plus anciens que la période de rétention
|
||||
3. **Pour chaque enregistrement** :
|
||||
- Vérification des étiquettes de protection
|
||||
- Si protégé → l'enregistrement est exempt
|
||||
4. **Suppression** selon le mode configuré (audio seulement ou complet)
|
||||
|
||||
## Dépannage
|
||||
|
||||
### La suppression automatique ne fonctionne pas
|
||||
|
||||
Contactez le support InnovA AI pour vérifier que le système est correctement configuré et actif.
|
||||
|
||||
### Trop d'enregistrements sont supprimés
|
||||
|
||||
1. Vérifiez vos étiquettes de protection et assurez-vous qu'elles sont appliquées aux enregistrements importants
|
||||
2. Contactez le support InnovA AI pour augmenter la période de rétention globale
|
||||
|
||||
### Les enregistrements archivés n'apparaissent pas
|
||||
|
||||
1. Activez le filtre **Enregistrements archivés** dans la barre latérale
|
||||
2. Vérifiez que le mode de suppression est bien "audio seulement" (le mode suppression complète ne crée pas d'archives)
|
||||
|
||||
## Sécurité et irréversibilité
|
||||
|
||||
- La suppression automatique est irréversible : les fichiers audio supprimés ne peuvent pas être récupérés
|
||||
- Les enregistrements protégés ne sont jamais supprimés automatiquement
|
||||
- L'activité de suppression est journalisée à des fins d'audit et de conformité Loi 25
|
||||
- Seuls les administrateurs peuvent configurer les politiques de rétention
|
||||
|
||||
Pour toute question ou problème concernant la suppression automatique, contactez le support InnovA AI.
|
||||
|
||||
---
|
||||
|
||||
Retour au [Guide d'administration](index.md) →
|
||||
103
client_docs/guide-admin/sso.md
Normal file
103
client_docs/guide-admin/sso.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# Authentification unique (SSO)
|
||||
|
||||
DictIA supporte l'authentification unique (SSO) via le protocole OpenID Connect (OIDC), vous permettant d'intégrer la plateforme avec votre fournisseur d'identité existant.
|
||||
|
||||
## Qu'est-ce que le SSO?
|
||||
|
||||
L'authentification unique permet à vos utilisateurs de se connecter à DictIA en utilisant les mêmes identifiants que pour les autres applications de votre organisation. Au lieu de gérer un mot de passe séparé pour DictIA, les utilisateurs se connectent via votre fournisseur d'identité centralisé.
|
||||
|
||||
### Avantages du SSO
|
||||
|
||||
- **Simplicité pour les utilisateurs** : Un seul ensemble d'identifiants pour toutes les applications
|
||||
- **Sécurité renforcée** : Politique de mots de passe centralisée, authentification multifacteur gérée par le fournisseur d'identité
|
||||
- **Administration simplifiée** : Gestion centralisée des accès, désactivation rapide des comptes
|
||||
- **Conformité Loi 25** : Meilleur contrôle de l'accès aux renseignements personnels grâce à la gestion centralisée des identités
|
||||
|
||||
## Fournisseurs d'identité supportés
|
||||
|
||||
DictIA est compatible avec tout fournisseur d'identité supportant le protocole OpenID Connect (OIDC), notamment :
|
||||
|
||||
- **Microsoft Entra ID** (anciennement Azure AD)
|
||||
- **Google Workspace**
|
||||
- **Keycloak**
|
||||
- **Auth0**
|
||||
- **Okta**
|
||||
- Et tout autre fournisseur OIDC compatible
|
||||
|
||||
## Configuration
|
||||
|
||||
La configuration technique du SSO (identifiants client, URL de découverte, redirections) est effectuée par l'équipe InnovA AI. Contactez le support InnovA AI pour mettre en place le SSO sur votre instance.
|
||||
|
||||
### Informations à fournir
|
||||
|
||||
Pour configurer le SSO, vous devrez fournir à InnovA AI :
|
||||
|
||||
- Le **fournisseur d'identité** que vous utilisez
|
||||
- Les **identifiants client** (Client ID et Client Secret) émis par votre fournisseur
|
||||
- L'**URL de découverte** OIDC de votre fournisseur
|
||||
- Les **domaines de courriel autorisés** pour l'auto-inscription (si souhaité)
|
||||
|
||||
### Options de configuration
|
||||
|
||||
Lors de la mise en place, vous pourrez choisir parmi les options suivantes :
|
||||
|
||||
**Auto-inscription** : Les nouveaux utilisateurs sont automatiquement créés lors de leur première connexion SSO, à condition que leur domaine de courriel soit autorisé. Cela élimine le besoin de créer manuellement chaque compte.
|
||||
|
||||
**Restriction de domaine** : Limitez l'accès SSO aux courriels de domaines spécifiques (ex. : votre-entreprise.com). Utile pour s'assurer que seuls les employés autorisés peuvent accéder à DictIA.
|
||||
|
||||
**Désactivation de la connexion par mot de passe** : Forcez l'utilisation exclusive du SSO pour tous les utilisateurs réguliers. Les administrateurs conservent la possibilité de se connecter par mot de passe comme mécanisme de secours.
|
||||
|
||||
## Expérience utilisateur
|
||||
|
||||
### Première connexion
|
||||
|
||||
Lors de sa première connexion via SSO, l'utilisateur :
|
||||
|
||||
1. Clique sur le bouton de connexion SSO sur la page de connexion de DictIA
|
||||
2. Est redirigé vers la page de connexion de votre fournisseur d'identité
|
||||
3. Se connecte avec ses identifiants habituels
|
||||
4. Est automatiquement redirigé vers DictIA avec une session active
|
||||
|
||||
Si l'auto-inscription est activée, le compte DictIA est créé automatiquement. Sinon, un administrateur doit d'abord créer le compte.
|
||||
|
||||
### Lier un compte existant
|
||||
|
||||
Les utilisateurs qui possèdent déjà un compte DictIA peuvent lier leur compte SSO :
|
||||
|
||||
1. Se connecter à DictIA avec leur mot de passe habituel
|
||||
2. Accéder à **Compte** → **Authentification unique**
|
||||
3. Cliquer sur **Lier le compte** pour associer leur identité SSO
|
||||
|
||||
Une fois lié, l'utilisateur peut se connecter via SSO ou par mot de passe (si la connexion par mot de passe n'est pas désactivée).
|
||||
|
||||
### Délier un compte SSO
|
||||
|
||||
Les utilisateurs peuvent délier leur compte SSO depuis **Compte** → **Authentification unique**. Les utilisateurs qui ont créé leur compte via SSO (sans mot de passe) doivent d'abord définir un mot de passe avant de pouvoir délier leur compte SSO.
|
||||
|
||||
## Considérations de sécurité
|
||||
|
||||
- Lorsqu'un utilisateur se connecte via SSO avec un courriel correspondant à un compte DictIA existant, les comptes sont automatiquement liés
|
||||
- La sécurité de l'accès SSO dépend de votre fournisseur d'identité : assurez-vous que celui-ci est correctement sécurisé (authentification multifacteur, politique de mots de passe forte)
|
||||
- Utilisez la restriction de domaine pour limiter l'accès aux seuls courriels de votre organisation
|
||||
|
||||
## Dépannage
|
||||
|
||||
### La connexion SSO échoue immédiatement
|
||||
|
||||
Les identifiants client ou l'URL de découverte sont peut-être incorrects. Contactez le support InnovA AI pour vérifier la configuration.
|
||||
|
||||
### Un utilisateur est créé sans adresse courriel
|
||||
|
||||
Certains fournisseurs d'identité ne retournent pas l'adresse courriel par défaut. Contactez le support InnovA AI pour ajuster la configuration des attributs (claims).
|
||||
|
||||
### Le domaine de courriel est rejeté
|
||||
|
||||
Vérifiez que le domaine de courriel de l'utilisateur fait partie de la liste des domaines autorisés. Contactez le support InnovA AI pour ajouter un domaine si nécessaire.
|
||||
|
||||
### Un compte SSO est déjà lié à un autre utilisateur
|
||||
|
||||
Chaque identité SSO ne peut être liée qu'à un seul compte DictIA. L'utilisateur peut délier son compte SSO depuis ses paramètres de compte, puis le relié à un autre compte.
|
||||
|
||||
---
|
||||
|
||||
Retour au [Guide d'administration](index.md) →
|
||||
82
client_docs/guide-admin/statistiques.md
Normal file
82
client_docs/guide-admin/statistiques.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# Statistiques du système
|
||||
|
||||
La section Statistiques du système transforme les données brutes en informations exploitables sur votre instance DictIA. D'un coup d'oeil, vous pouvez voir combien d'utilisateurs vous desservez, combien d'enregistrements ils ont créés, combien de stockage ils consomment et si tout se traite correctement.
|
||||
|
||||
## Vue d'ensemble des métriques clés
|
||||
|
||||
Quatre cartes en haut de la page de statistiques vous donnent un aperçu immédiat de l'envergure de votre système :
|
||||
|
||||
- **Utilisateurs totaux** : La taille de votre base d'utilisateurs actuelle
|
||||
- **Enregistrements totaux** : Le contenu cumulatif dans votre système
|
||||
- **Stockage total** : L'espace disque réellement consommé
|
||||
- **Requêtes totales** : L'utilisation de la Recherche IA (lorsque activée)
|
||||
|
||||
Ces chiffres racontent une histoire sur la santé et la croissance de votre instance. Une base d'utilisateurs en croissance avec une augmentation proportionnelle des enregistrements suggère une adoption saine. Un stockage qui croît plus rapidement que les enregistrements peut indiquer que les utilisateurs téléversent des fichiers plus longs.
|
||||
|
||||
## Distribution des statuts d'enregistrement
|
||||
|
||||
Cette section décompose vos enregistrements en quatre états :
|
||||
|
||||
- **Complétés** : Entièrement traités et prêts à l'utilisation. Cela devrait représenter la grande majorité de votre contenu.
|
||||
- **En traitement** : En cours de transcription ou d'analyse.
|
||||
- **En attente** : En file d'attente. Un nombre élevé constant pourrait indiquer une surcharge du système.
|
||||
- **En erreur** : Des problèmes ont été rencontrés. Ces cas méritent toujours une investigation.
|
||||
|
||||
Dans un système en santé, vous verrez principalement des enregistrements complétés avec peut-être quelques-uns en traitement à tout moment.
|
||||
|
||||
## Analyse du stockage
|
||||
|
||||
La section **Principaux utilisateurs par stockage** révèle qui consomme le plus de ressources. Chaque utilisateur est listé avec sa consommation totale de stockage et son nombre d'enregistrements, vous donnant le contexte pour comprendre s'ils ont plusieurs petits fichiers ou quelques gros fichiers.
|
||||
|
||||
Ces informations sont précieuses pour la planification de la capacité. Si un utilisateur consomme un stockage disproportionné, il peut être utile de comprendre son cas d'utilisation et d'adapter les politiques en conséquence.
|
||||
|
||||
## Comprendre les tendances d'utilisation
|
||||
|
||||
Les statistiques ne sont pas que des chiffres : ce sont des informations qui attendent d'être découvertes.
|
||||
|
||||
- Les pics soudains d'enregistrements peuvent coïncider avec des lancements de projets ou des périodes de pointe
|
||||
- Une croissance du stockage qui dépasse celle des enregistrements pourrait indiquer des fichiers audio plus longs ou de meilleure qualité
|
||||
- Un suivi régulier vous aide à repérer les tendances avant qu'elles ne deviennent des problèmes
|
||||
|
||||
## Planification de la capacité
|
||||
|
||||
Les statistiques du système sont votre boule de cristal pour les besoins d'infrastructure. Les tendances de croissance du stockage vous indiquent quand une augmentation de capacité sera nécessaire. Utilisez ces informations de manière proactive pour anticiper les besoins. Contactez le support InnovA AI si vous observez une croissance rapide qui pourrait nécessiter une augmentation des ressources.
|
||||
|
||||
## Statistiques d'utilisation des jetons
|
||||
|
||||
La section Utilisation des jetons offre une visibilité sur la consommation d'API de modèles d'IA à travers votre instance.
|
||||
|
||||
**Cartes de résumé** : Affichent l'utilisation quotidienne, les totaux mensuels, les coûts estimés et les utilisateurs approchant de leurs limites de budget.
|
||||
|
||||
**Graphiques quotidiens et mensuels** : Des graphiques interactifs affichent les tendances de consommation de jetons sur les 30 derniers jours et les 12 derniers mois.
|
||||
|
||||
**Ventilation par utilisateur** : Un tableau détaillé montre la consommation mensuelle de jetons de chaque utilisateur avec sa limite de budget (si définie). Les barres de progression indiquent le pourcentage utilisé :
|
||||
|
||||
- **Vert** : Sous 80 % du budget
|
||||
- **Jaune** : Entre 80 et 100 % (zone d'avertissement)
|
||||
- **Rouge** : À 100 % ou plus (bloqué)
|
||||
|
||||
Utilisez ces statistiques pour identifier les gros utilisateurs, valider les allocations de budget et prévoir les coûts d'API. Si certains utilisateurs atteignent régulièrement leurs limites, vous pourriez avoir besoin d'augmenter leurs budgets ou d'examiner leurs habitudes d'utilisation. Consultez la [Gestion du budget de jetons](gestion-utilisateurs.md#gestion-du-budget-de-jetons-ia) pour définir les limites individuelles.
|
||||
|
||||
## Statistiques d'utilisation de la transcription
|
||||
|
||||
La section Utilisation de la transcription offre une visibilité sur la consommation de services de transcription vocale, distincte de l'utilisation des jetons.
|
||||
|
||||
**Cartes de résumé** :
|
||||
|
||||
- **Minutes aujourd'hui** : Minutes de transcription utilisées aujourd'hui par tous les utilisateurs
|
||||
- **Ce mois-ci** : Total des minutes transcrites dans le mois civil en cours
|
||||
- **Coût mensuel** : Coûts estimés basés sur le service de transcription utilisé
|
||||
- **Alertes de budget** : Nombre d'utilisateurs approchant (80 %+) ou dépassant (100 %) leurs budgets
|
||||
|
||||
**Ventilation par utilisateur** : Une liste détaillée montre l'utilisation mensuelle de transcription de chaque utilisateur avec sa limite de budget (si définie). Les barres de progression indiquent la consommation :
|
||||
|
||||
- **Vert** : Sous 80 % du budget
|
||||
- **Jaune** : Entre 80 et 100 % (zone d'avertissement)
|
||||
- **Rouge** : À 100 % ou plus (bloqué pour les nouvelles transcriptions)
|
||||
|
||||
Utilisez ces statistiques pour surveiller les tendances d'utilisation et valider les allocations de budget. Consultez la [Gestion du budget de transcription](gestion-utilisateurs.md#gestion-du-budget-de-transcription) pour définir les limites individuelles.
|
||||
|
||||
---
|
||||
|
||||
Suivant : [Paramètres du système](parametres-systeme.md) →
|
||||
197
client_docs/guide-utilisateur/application-mobile.md
Normal file
197
client_docs/guide-utilisateur/application-mobile.md
Normal file
@@ -0,0 +1,197 @@
|
||||
# Application mobile
|
||||
|
||||
DictIA est une application web progressive (PWA) qui peut etre installee sur votre appareil pour une experience similaire a une application native, avec la prevention de la mise en veille de l'ecran pendant l'enregistrement.
|
||||
|
||||
## Avantages de l'application mobile
|
||||
|
||||
- **Installable** : ajoutez DictIA a votre ecran d'accueil comme une application native.
|
||||
- **Chargement rapide** : les ressources mises en cache se chargent instantanement.
|
||||
- **Prevention de la mise en veille** : empeche l'ecran de s'eteindre automatiquement pendant l'enregistrement.
|
||||
- **Pas de boutique d'applications** : installez directement depuis votre navigateur.
|
||||
- **Mises a jour automatiques** : vous obtenez toujours la derniere version.
|
||||
|
||||
## Installer DictIA sur votre appareil
|
||||
|
||||
### Sur Android (Chrome ou Edge)
|
||||
|
||||
1. **Ouvrez DictIA** dans Chrome ou Edge.
|
||||
2. Recherchez l'invite **« Ajouter a l'ecran d'accueil »** en bas de l'ecran.
|
||||
3. Appuyez sur **Ajouter** ou **Installer**.
|
||||
4. Si l'invite n'apparait pas :
|
||||
- Appuyez sur le menu a trois points (⋮) dans le navigateur.
|
||||
- Selectionnez **« Ajouter a l'ecran d'accueil »** ou **« Installer l'application »**.
|
||||
- Suivez les instructions.
|
||||
5. **Lancez** l'application depuis votre ecran d'accueil.
|
||||
|
||||
!!! tip "Invite d'installation"
|
||||
Si vous ne voyez pas l'invite d'installation, visitez DictIA quelques fois. Le navigateur propose l'installation apres avoir detecte une utilisation reguliere.
|
||||
|
||||
### Sur iOS (Safari)
|
||||
|
||||
1. **Ouvrez DictIA** dans Safari.
|
||||
2. Appuyez sur le bouton **Partager** (icone carre avec fleche vers le haut) en bas de l'ecran.
|
||||
3. Faites defiler et appuyez sur **« Sur l'ecran d'accueil »**.
|
||||
4. Modifiez le nom si desire et appuyez sur **Ajouter**.
|
||||
5. **Lancez** l'application depuis votre ecran d'accueil.
|
||||
|
||||
!!! note "Limitations iOS"
|
||||
- La prevention de la mise en veille necessite iOS 16.4 ou superieur.
|
||||
- L'execution en arriere-plan est plus limitee que sur Android.
|
||||
- Certaines fonctionnalites fonctionnent mieux sur iOS 17 et versions ulterieures.
|
||||
|
||||
### Sur ordinateur de bureau (Chrome, Edge ou Brave)
|
||||
|
||||
1. **Ouvrez DictIA** dans votre navigateur.
|
||||
2. Recherchez l'**icone d'installation** (⊕) dans la barre d'adresse.
|
||||
3. Cliquez dessus et selectionnez **Installer**.
|
||||
4. Si l'icone n'apparait pas :
|
||||
- Cliquez sur le menu a trois points.
|
||||
- Selectionnez **« Installer DictIA »** ou **« Ajouter aux applications »**.
|
||||
5. **Lancez** depuis votre menu d'applications ou votre raccourci de bureau.
|
||||
|
||||
## Fonctionnalites de l'application mobile
|
||||
|
||||
### Prevention de la mise en veille de l'ecran
|
||||
|
||||
Empeche l'ecran de votre appareil de s'eteindre automatiquement pendant l'enregistrement :
|
||||
|
||||
- **Activation automatique** : s'active des que vous commencez un enregistrement.
|
||||
- **Ecran toujours allume** : empeche la mise en veille tant que l'application est au premier plan.
|
||||
- **Recuperation automatique** : se reactive si l'ecran se reveille pendant un enregistrement.
|
||||
- **Consideration de la batterie** : actif uniquement pendant les sessions d'enregistrement.
|
||||
|
||||
### Notifications persistantes
|
||||
|
||||
Affiche l'etat de l'enregistrement dans votre barre de notifications (mobile uniquement) :
|
||||
|
||||
- Rappel visuel que l'enregistrement est actif
|
||||
- Retour rapide a l'application en un seul appui
|
||||
- Silencieuse : pas de son ni de vibration
|
||||
|
||||
### Acces hors connexion
|
||||
|
||||
Une fois installee, DictIA met en cache les fichiers essentiels pour un acces hors ligne :
|
||||
|
||||
- Interface de l'application
|
||||
- Feuilles de style et polices
|
||||
- Code de l'application
|
||||
|
||||
!!! info "Limites hors connexion"
|
||||
La transcription et les fonctionnalites d'IA necessitent une connexion internet. L'acces hors ligne permet de consulter l'interface, mais les appels au serveur ne sont pas disponibles.
|
||||
|
||||
## Enregistrement sur mobile — Limitation importante
|
||||
|
||||
!!! danger "Gardez l'application visible sur les appareils mobiles"
|
||||
**Les navigateurs mobiles (Chrome, Safari, etc.) suspendent l'enregistrement audio lorsque l'application est reduite ou l'ecran verrouille.** Il s'agit d'une limitation fondamentale des navigateurs qui ne peut pas etre contournee.
|
||||
|
||||
**Ce que cela signifie :**
|
||||
|
||||
- L'enregistrement **se met en pause** si vous reduisez la fenetre.
|
||||
- L'enregistrement **se met en pause** si vous verrouillez votre ecran.
|
||||
- L'enregistrement **se met en pause** si vous passez a une autre application.
|
||||
- L'enregistrement **continue** si vous gardez DictIA visible au premier plan.
|
||||
- La prevention de la mise en veille **empeche l'ecran de s'eteindre automatiquement** pendant que DictIA est visible.
|
||||
|
||||
**Pour les longues reunions sur mobile :**
|
||||
|
||||
1. Gardez DictIA ouvert et visible (ne reduisez pas, ne verrouillez pas, ne changez pas d'application).
|
||||
2. Ou utilisez l'enregistreur vocal natif de votre telephone, puis televersez le fichier dans DictIA par la suite.
|
||||
|
||||
**Les navigateurs de bureau fonctionnent differemment** : l'enregistrement continue meme lorsque la fenetre est reduite.
|
||||
|
||||
**Si vous reduisez accidentellement l'application :**
|
||||
|
||||
- L'audio de l'enregistrement se met en pause (silence).
|
||||
- Le minuteur continue de compter.
|
||||
- Lorsque vous revenez a l'application, l'enregistrement reprend.
|
||||
- Un silence sera present dans l'enregistrement final.
|
||||
|
||||
## Permissions
|
||||
|
||||
### Permissions requises
|
||||
|
||||
- **Acces au microphone** : demande lors du premier enregistrement. Actif uniquement pendant l'enregistrement.
|
||||
- **Acces a l'audio systeme** (ordinateur de bureau) : pour capturer l'audio des onglets de navigateur ou des applications.
|
||||
|
||||
### Permissions recommandees
|
||||
|
||||
- **Notifications** (mobile) : pour afficher un rappel visuel pendant l'enregistrement actif. Demande lors du premier enregistrement sur mobile.
|
||||
|
||||
!!! tip "Reactiver les permissions"
|
||||
Si vous refusez accidentellement une permission, vous pouvez la reinitialiser dans les parametres de votre navigateur ou appareil :
|
||||
|
||||
- **Android Chrome** : Parametres > Parametres du site > URL de DictIA > Permissions
|
||||
- **iOS Safari** : Reglages > Safari > URL de DictIA > Permissions
|
||||
- **Ordinateur** : Cliquez sur l'icone de cadenas dans la barre d'adresse > Permissions
|
||||
|
||||
## Compatibilite des navigateurs
|
||||
|
||||
### Navigateurs mobiles
|
||||
|
||||
| Fonctionnalite | Chrome Android | Safari iOS | Samsung Internet |
|
||||
|----------------|----------------|------------|------------------|
|
||||
| Installation | Android 5+ | iOS 11.3+ | 4.0+ |
|
||||
| Prevention mise en veille | Chrome 84+ | iOS 16.4+ | 13.0+ |
|
||||
| Notifications | Chrome 42+ | iOS 16.4+ | 4.0+ |
|
||||
|
||||
### Navigateurs de bureau
|
||||
|
||||
| Fonctionnalite | Chrome | Edge | Brave | Firefox | Safari |
|
||||
|----------------|--------|------|-------|---------|--------|
|
||||
| Installation | 73+ | 79+ | Toutes | Limitee | Limitee |
|
||||
| Prevention mise en veille | 84+ | 84+ | Toutes | Non | 16.4+ |
|
||||
|
||||
!!! info "Firefox et Safari sur ordinateur"
|
||||
Firefox et Safari ont un support limite pour l'installation en tant qu'application sur ordinateur, mais toutes les fonctionnalites principales fonctionnent dans le navigateur.
|
||||
|
||||
## Depannage
|
||||
|
||||
### L'enregistrement s'arrete lorsque l'ecran se verrouille
|
||||
|
||||
C'est un comportement attendu sur les navigateurs mobiles. Gardez l'application visible ou utilisez l'enregistreur vocal natif de votre telephone.
|
||||
|
||||
### L'installation n'est pas proposee
|
||||
|
||||
- Visitez DictIA plusieurs fois (le navigateur peut necessiter plusieurs visites).
|
||||
- Essayez un autre navigateur (Chrome ou Edge offrent le meilleur support).
|
||||
- Videz le cache du navigateur et rechargez.
|
||||
|
||||
## Bonnes pratiques
|
||||
|
||||
### Pour l'enregistrement mobile
|
||||
|
||||
1. **Gardez l'application visible** : ne reduisez pas, ne verrouillez pas, ne changez pas d'application.
|
||||
2. **Gardez votre telephone branche** pour les longs enregistrements.
|
||||
3. **Fermez les applications inutiles** pour liberer de la memoire.
|
||||
4. **Evitez de recevoir des appels** pendant l'enregistrement (cela mettra en pause l'enregistrement).
|
||||
5. **Pour les longues reunions** : envisagez d'utiliser l'enregistreur natif de votre telephone.
|
||||
|
||||
### Pour des performances optimales
|
||||
|
||||
1. **Installez DictIA comme application** pour de meilleures performances.
|
||||
2. **Gardez votre navigateur a jour** pour beneficier des dernieres fonctionnalites.
|
||||
3. **Surveillez le stockage** dans les parametres de votre appareil.
|
||||
|
||||
## Desinstaller l'application
|
||||
|
||||
### Android
|
||||
|
||||
1. Appuyez longuement sur l'icone de l'application sur l'ecran d'accueil.
|
||||
2. Selectionnez **Infos sur l'appli** ou glissez vers **Supprimer**.
|
||||
3. Appuyez sur **Desinstaller**.
|
||||
|
||||
### iOS
|
||||
|
||||
1. Appuyez longuement sur l'icone de l'application.
|
||||
2. Selectionnez **Supprimer l'app**.
|
||||
3. Confirmez la suppression.
|
||||
|
||||
### Ordinateur de bureau
|
||||
|
||||
1. Ouvrez l'application installee.
|
||||
2. Cliquez sur le menu a trois points.
|
||||
3. Selectionnez **Desinstaller DictIA**.
|
||||
|
||||
---
|
||||
|
||||
Si vous avez des questions sur les fonctionnalites de l'application mobile, contactez le support InnovA AI.
|
||||
115
client_docs/guide-utilisateur/dossiers.md
Normal file
115
client_docs/guide-utilisateur/dossiers.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# Dossiers
|
||||
|
||||
Les dossiers offrent un moyen simple d'organiser vos enregistrements en groupes logiques. Contrairement aux etiquettes qui peuvent etre appliquees plusieurs fois pour categoriser le contenu, chaque enregistrement appartient a un seul dossier (ou a aucun). Pensez aux dossiers comme a des repertoires sur votre ordinateur : une facon claire et hierarchique de regrouper les enregistrements lies.
|
||||
|
||||
## Creer des dossiers
|
||||
|
||||
Accedez a la gestion des dossiers depuis **Parametres du compte > Dossiers**.
|
||||
|
||||
Cliquez sur **Creer un dossier** pour ajouter un nouveau dossier. Chaque dossier possede :
|
||||
|
||||
- **Nom** : un nom court et descriptif (obligatoire).
|
||||
- **Couleur** : un identifiant visuel affiche sur les pastilles de dossier et dans la barre laterale.
|
||||
|
||||
### Parametres du dossier
|
||||
|
||||
Chaque dossier peut avoir des parametres personnalises qui s'appliquent aux enregistrements qui y sont places :
|
||||
|
||||
| Parametre | Description |
|
||||
|-----------|-------------|
|
||||
| **Consigne personnalisee** | Instructions de resume IA specifiques a ce dossier |
|
||||
| **Langue par defaut** | Langue de transcription pour les nouveaux enregistrements |
|
||||
| **Min/Max locuteurs** | Indications sur le nombre de locuteurs pour l'identification |
|
||||
| **Vocabulaire personnalise** | Liste de mots ou expressions a prioriser par le moteur de transcription |
|
||||
| **Consigne de transcription** | Description du contexte pour guider la transcription |
|
||||
| **Jours de retention** | Duree de conservation specifique au dossier |
|
||||
| **Protection** | Exempter le contenu du dossier de la suppression automatique |
|
||||
|
||||
## Utiliser les dossiers
|
||||
|
||||
### Selecteur de dossier dans la barre laterale
|
||||
|
||||
La barre laterale inclut un menu deroulant de dossiers en haut. Utilisez-le pour :
|
||||
|
||||
- **Filtrer par dossier** : selectionnez un dossier pour n'afficher que ses enregistrements.
|
||||
- **Voir tous les enregistrements** : selectionnez « Tous les dossiers ».
|
||||
- **Voir les enregistrements non classes** : selectionnez « Aucun dossier ».
|
||||
|
||||
### Deplacer des enregistrements dans les dossiers
|
||||
|
||||
Plusieurs facons d'assigner un enregistrement a un dossier :
|
||||
|
||||
1. **Pendant le televersement** : selectionnez un dossier avant de televerser.
|
||||
2. **Depuis le detail de l'enregistrement** : utilisez le selecteur de dossier dans l'en-tete.
|
||||
3. **Operations en lot** : selectionnez plusieurs enregistrements et utilisez l'action de dossier.
|
||||
|
||||
### Pastilles de dossier
|
||||
|
||||
Les enregistrements affichent une petite pastille coloree indiquant leur dossier. Elle apparait dans :
|
||||
|
||||
- La liste des enregistrements dans la barre laterale
|
||||
- L'en-tete du detail de l'enregistrement
|
||||
- Les resultats de recherche
|
||||
|
||||
Cliquez sur la pastille pour filtrer rapidement par ce dossier.
|
||||
|
||||
## Dossiers de groupe
|
||||
|
||||
Si votre organisation utilise les groupes, les dossiers peuvent etre partages :
|
||||
|
||||
- **Dossiers personnels** : visibles uniquement par vous.
|
||||
- **Dossiers de groupe** : partages avec tous les membres du groupe.
|
||||
|
||||
Les dossiers de groupe peuvent avoir le partage automatique active : lorsque vous deplacez un enregistrement dans un dossier de groupe, tous les membres du groupe recoivent automatiquement l'acces.
|
||||
|
||||
## Dossiers vs Etiquettes
|
||||
|
||||
Les deux outils aident a organiser vos enregistrements, mais servent des objectifs differents :
|
||||
|
||||
| Aspect | Dossiers | Etiquettes |
|
||||
|--------|----------|------------|
|
||||
| **Attribution** | Un seul dossier par enregistrement | Plusieurs etiquettes par enregistrement |
|
||||
| **Objectif** | Organisation principale | Categorisation et filtrage |
|
||||
| **Parametres** | Consigne IA, langue, retention | Consigne IA, langue, retention |
|
||||
| **Visuel** | Pastille, filtre dans la barre laterale | Pastilles colorees |
|
||||
|
||||
**Utilisez les dossiers pour** : l'organisation par projet, la separation par client, les types de reunions.
|
||||
|
||||
**Utilisez les etiquettes pour** : les preoccupations transversales, le suivi de statut, la categorisation par sujet.
|
||||
|
||||
Vous pouvez utiliser les deux ensemble : placez un enregistrement dans le dossier « Client A » et etiquetez-le avec « Actions a suivre » et « Suivi requis ».
|
||||
|
||||
## Bonnes pratiques
|
||||
|
||||
### Conventions de nommage
|
||||
|
||||
Choisissez des noms de dossier clairs et coherents :
|
||||
|
||||
- **Par projet** : « Refonte du site web », « Planification T1 », « Lancement produit »
|
||||
- **Par client** : « Acme Corp », « Beta Inc », « Gamma LLC »
|
||||
- **Par type** : « Meles d'equipe », « Appels clients », « Entrevues »
|
||||
|
||||
### Consignes de dossier
|
||||
|
||||
Utilisez des consignes specifiques au dossier pour des resumes coherents. Par exemple, pour les reunions clients :
|
||||
|
||||
```
|
||||
Creer un resume avec :
|
||||
- Sujets discutes
|
||||
- Demandes et preoccupations du client
|
||||
- Engagements pris
|
||||
- Prochaines etapes avec responsables
|
||||
```
|
||||
|
||||
### Strategies de retention
|
||||
|
||||
Definissez la retention au niveau du dossier selon les besoins :
|
||||
|
||||
- **Projets actifs** : aucune limite de retention
|
||||
- **Projets termines** : 90 jours apres la cloture
|
||||
- **Reunions de routine** : 30 jours
|
||||
- **Enregistrements de conformite** : utilisez le drapeau de protection
|
||||
|
||||
---
|
||||
|
||||
Suivant : [Groupes](groupes.md)
|
||||
197
client_docs/guide-utilisateur/enregistrement.md
Normal file
197
client_docs/guide-utilisateur/enregistrement.md
Normal file
@@ -0,0 +1,197 @@
|
||||
# Enregistrement et televersement
|
||||
|
||||
DictIA offre deux facons d'ajouter du contenu a votre bibliotheque : televerser des fichiers audio existants ou enregistrer directement dans votre navigateur. Les deux methodes incluent les memes fonctionnalites d'organisation, d'etiquetage et de traitement.
|
||||
|
||||
## Acceder a l'ecran d'enregistrement
|
||||
|
||||
Cliquez sur le bouton **+ Nouvel enregistrement** dans la barre de navigation superieure pour ouvrir l'interface d'enregistrement.
|
||||
|
||||
## Televerser des fichiers audio
|
||||
|
||||
L'interface de televersement propose une zone de glisser-deposer en haut de l'ecran. Vous pouvez :
|
||||
|
||||
- **Glisser** des fichiers audio directement depuis votre explorateur de fichiers vers cette zone.
|
||||
- **Cliquer** sur la zone pour ouvrir un selecteur de fichiers.
|
||||
|
||||
### Formats pris en charge
|
||||
|
||||
DictIA prend en charge un large eventail de formats audio et video :
|
||||
|
||||
- **Audio** : MP3, WAV, M4A, FLAC, AAC, OGG
|
||||
- **Video** : MP4, MOV, AVI (DictIA extrait et traite la piste audio)
|
||||
- **Mobile** : AMR, 3GP, 3GPP
|
||||
|
||||
La taille maximale par defaut est de 500 Mo. Votre administrateur peut modifier cette limite.
|
||||
|
||||
!!! info "Compression audio automatique"
|
||||
DictIA compresse automatiquement les fichiers sans perte (WAV, AIFF) pour economiser l'espace de stockage. Un fichier WAV de 500 Mo devient generalement environ 50 Mo apres compression. Les formats deja compresses comme le MP3 sont conserves tels quels.
|
||||
|
||||
Lorsque vous televersez un fichier, il apparait immediatement dans la file d'attente avec une barre de progression. Une fois televerse, vous pouvez ajouter des etiquettes, definir un titre personnalise et configurer les options de traitement avant de lancer la transcription.
|
||||
|
||||
## Enregistrer de l'audio en direct
|
||||
|
||||
Sous la zone de televersement, trois modes d'enregistrement sont disponibles :
|
||||
|
||||
### Enregistrement au microphone
|
||||
|
||||
Le bouton rouge du microphone capture l'audio depuis votre microphone selectionne. Ce mode est ideal pour :
|
||||
|
||||
- Les reunions en personne
|
||||
- Les notes vocales personnelles
|
||||
- Les entrevues
|
||||
|
||||
Votre navigateur vous demandera l'autorisation d'acceder au microphone si elle n'a pas deja ete accordee. Vous pouvez selectionner quel microphone utiliser si plusieurs appareils sont connectes.
|
||||
|
||||
### Enregistrement de l'audio systeme
|
||||
|
||||
Le bouton bleu capture tout le son joue par votre ordinateur. Ce mode est ideal pour :
|
||||
|
||||
- Les reunions en ligne lorsque vous ecoutez principalement
|
||||
- Les webinaires et presentations en ligne
|
||||
- Les balados ou tout contenu audio diffuse sur votre ordinateur
|
||||
|
||||
Lorsque vous cliquez sur ce bouton, une fenetre de partage d'ecran apparait. Il est essentiel de cocher la case **« Partager l'audio du systeme »** pour que l'enregistrement fonctionne.
|
||||
|
||||
### Enregistrement combine (microphone + systeme)
|
||||
|
||||
Le bouton violet enregistre simultanement votre microphone et l'audio systeme dans une seule piste synchronisee. C'est le mode recommande pour les reunions en ligne ou vous participez activement, car il capture les deux cotes de la conversation.
|
||||
|
||||
## Activer l'enregistrement de l'audio systeme
|
||||
|
||||
### Enregistrer depuis un onglet du navigateur
|
||||
|
||||
Choisissez l'option **onglet** si l'audio provient d'un onglet specifique (YouTube, Google Meet, Zoom dans le navigateur, etc.).
|
||||
|
||||
Selectionnez l'onglet contenant votre source audio, puis **cochez la case « Partager l'audio de l'onglet »** en bas de la fenetre de dialogue. Sans cette case cochee, vous n'enregistrerez que du silence.
|
||||
|
||||
### Enregistrer depuis l'ecran complet
|
||||
|
||||
Choisissez l'option **ecran** si l'audio provient d'applications de bureau ou de plusieurs sources :
|
||||
|
||||
- Applications comme Zoom, Microsoft Teams, Skype ou Discord
|
||||
- Audio de plusieurs onglets simultanement
|
||||
- Sons systeme
|
||||
|
||||
Selectionnez votre ecran (peu importe lequel si vous avez plusieurs moniteurs, DictIA n'enregistre que l'audio). **Cochez la case « Partager l'audio du systeme »** en bas de la fenetre.
|
||||
|
||||
!!! warning "Limitation macOS"
|
||||
L'option « Partager l'audio du systeme » est disponible sur Windows et Linux, mais peut ne pas etre disponible sur macOS en raison de restrictions du systeme d'exploitation. Les utilisateurs macOS peuvent generalement enregistrer l'audio des onglets de navigateur uniquement.
|
||||
|
||||
## Pendant l'enregistrement
|
||||
|
||||
### Surveillance audio en temps reel
|
||||
|
||||
L'interface affiche des visualiseurs audio en direct pour chaque source active, confirmant que l'audio est bien capture. Un minuteur affiche le temps ecoule et une estimation de la taille du fichier.
|
||||
|
||||
### Prise de notes en direct avec Markdown
|
||||
|
||||
Vous pouvez prendre des notes structurees pendant l'enregistrement. La zone de notes supporte le format Markdown complet avec une barre de formatage. Vos notes sont enregistrees automatiquement et seront associees a l'enregistrement.
|
||||
|
||||
Utilisations courantes des notes en direct :
|
||||
|
||||
- Capturer les actions a suivre et les echeances
|
||||
- Noter les noms des participants et leurs roles
|
||||
- Documenter les decisions et leur justification
|
||||
- Ajouter du contexte non perceptible dans l'audio
|
||||
|
||||
## Finaliser votre enregistrement
|
||||
|
||||
Apres avoir arrete un enregistrement ou selectionne un fichier televerse, l'ecran de finalisation vous permet d'ajouter des metadonnees et de configurer les options de traitement.
|
||||
|
||||
### Ajouter des etiquettes
|
||||
|
||||
Les etiquettes sont des pastilles colorees que vous pouvez selectionner pour categoriser votre enregistrement. Vous pouvez appliquer plusieurs etiquettes a un seul enregistrement.
|
||||
|
||||
!!! tip "Selectionnez vos etiquettes avant le televersement"
|
||||
Les etiquettes peuvent avoir des consignes IA personnalisees associees. En les selectionnant avant le traitement, les bonnes consignes de resume seront appliquees automatiquement.
|
||||
|
||||
**Empilage intelligent des consignes** : lorsque vous selectionnez plusieurs etiquettes, leurs consignes sont concatenees dans l'ordre de selection. Vous pouvez ainsi combiner une etiquette generale (ex. : « Reunions ») avec une etiquette specifique (ex. : un client ou un projet).
|
||||
|
||||
### Options avancees de transcription
|
||||
|
||||
Si votre administrateur a configure un service de transcription avec identification des locuteurs, une section **Options avancees** est disponible. Vous pouvez y specifier :
|
||||
|
||||
- **La langue de transcription** si votre contenu n'est pas en francais.
|
||||
- **Le nombre minimum et maximum de locuteurs** pour une meilleure precision de l'identification.
|
||||
|
||||
#### Vocabulaire personnalise
|
||||
|
||||
Le champ **Vocabulaire personnalise** vous permet de fournir une liste de mots ou expressions (separees par des virgules) que le moteur de transcription doit prioriser. Utile pour les termes techniques, noms propres, acronymes ou noms de marques.
|
||||
|
||||
Exemple : `DictIA, InnovA AI, WhisperX`
|
||||
|
||||
#### Consigne de transcription
|
||||
|
||||
Le champ **Consigne de transcription** fournit du contexte au moteur de transcription. Par exemple : « Ceci est une reunion sur les outils de transcription audio par IA. »
|
||||
|
||||
!!! info "Hierarchie de priorite"
|
||||
Les valeurs saisies dans le formulaire de televersement ont priorite sur les parametres par defaut des etiquettes, qui ont priorite sur ceux des dossiers, qui ont priorite sur vos parametres personnels dans les [parametres du compte](parametres.md).
|
||||
|
||||
### Actions finales
|
||||
|
||||
- **Televerser / Lancer le traitement** : demarre la transcription immediatement avec vos parametres.
|
||||
- **Supprimer** : supprime l'enregistrement sans le sauvegarder.
|
||||
|
||||
## Recuperation apres un incident
|
||||
|
||||
DictIA sauvegarde automatiquement vos enregistrements en cours toutes les 5 secondes dans le stockage de votre navigateur. Si votre navigateur plante, si l'onglet se ferme accidentellement ou si votre ordinateur redemarre, vous ne perdrez pas votre travail.
|
||||
|
||||
**Ce qui est recupere :**
|
||||
|
||||
- Tout l'audio enregistre jusqu'au moment de l'interruption
|
||||
- Le mode d'enregistrement (microphone, audio systeme ou combine)
|
||||
- Les notes saisies
|
||||
- Les etiquettes et parametres selectionnes
|
||||
|
||||
**Comment ca fonctionne :**
|
||||
|
||||
1. Lorsque vous revenez sur DictIA apres un incident, une fenetre de recuperation apparait automatiquement.
|
||||
2. Examinez les details de l'enregistrement (duree, taille, horodatage).
|
||||
3. Cliquez sur **Restaurer l'enregistrement** pour continuer, ou **Supprimer** pour recommencer.
|
||||
|
||||
!!! warning "Limitations"
|
||||
- Les donnees de recuperation sont stockees dans le navigateur (par navigateur, par appareil).
|
||||
- Effacer les donnees du navigateur supprime les informations de recuperation.
|
||||
- Jusqu'a 5 secondes d'audio peuvent etre perdues.
|
||||
|
||||
## Bonnes pratiques
|
||||
|
||||
### Enregistrement sur appareils mobiles
|
||||
|
||||
!!! danger "Gardez l'application visible sur les appareils mobiles"
|
||||
Les navigateurs mobiles (Chrome, Safari, etc.) **mettent en pause l'enregistrement audio** lorsque vous reduisez l'application ou verrouillez votre ecran. Il s'agit d'une limitation fondamentale des navigateurs.
|
||||
|
||||
**Pour enregistrer avec succes sur mobile :**
|
||||
|
||||
- Gardez DictIA **visible** au premier plan en tout temps.
|
||||
- Ne **reduisez pas** la fenetre et ne changez pas d'application.
|
||||
- Ne **verrouillez pas** votre ecran pendant l'enregistrement.
|
||||
|
||||
**Pour les longues reunions sur mobile :**
|
||||
|
||||
1. Gardez DictIA ouvert et visible (le verrouillage d'ecran est empeche automatiquement).
|
||||
2. Ou utilisez l'enregistreur vocal natif de votre telephone, puis televersez le fichier dans DictIA.
|
||||
|
||||
**Les navigateurs de bureau n'ont pas cette limitation** : l'enregistrement continue meme lorsque la fenetre est reduite.
|
||||
|
||||
Pour une meilleure experience mobile, installez DictIA comme application mobile. Consultez le [guide de l'application mobile](application-mobile.md).
|
||||
|
||||
### Optimiser la qualite audio
|
||||
|
||||
- Trouvez un endroit calme avec un minimum d'echo et de bruit de fond.
|
||||
- Positionnez votre microphone a 15-30 cm de votre bouche.
|
||||
- Pour l'enregistrement d'audio systeme, fermez les applications inutiles qui pourraient produire des sons de notification.
|
||||
- Lors de l'enregistrement combine (microphone + systeme), **utilisez des ecouteurs** pour eviter l'echo.
|
||||
|
||||
!!! info "Enregistrements avec telephone"
|
||||
Les telephones intelligents ont des algorithmes de reduction de bruit qui peuvent filtrer les voix eloignees lors d'enregistrements multi-locuteurs. Pour de meilleurs resultats, utilisez un microphone externe ou placez le telephone a distance egale de tous les locuteurs.
|
||||
|
||||
### Organisation des le depart
|
||||
|
||||
- Incluez des informations cles dans les titres (type de reunion, sujet, projet).
|
||||
- Appliquez les etiquettes immediatement pendant que le contexte est frais.
|
||||
- Utilisez la prise de notes en direct pour capturer les informations non audibles (noms des participants, decisions visuelles partagees, etc.).
|
||||
|
||||
---
|
||||
|
||||
Suivant : [Travailler avec les transcriptions](transcriptions.md)
|
||||
139
client_docs/guide-utilisateur/groupes.md
Normal file
139
client_docs/guide-utilisateur/groupes.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# Collaboration en groupe
|
||||
|
||||
Les groupes transforment la facon dont les equipes collaborent dans DictIA en permettant un partage automatique sans effort manuel. Lorsque votre organisation utilise des groupes, les enregistrements atteignent automatiquement les bonnes personnes au bon moment.
|
||||
|
||||
## Comprendre les groupes
|
||||
|
||||
Les groupes dans DictIA sont des ensembles d'utilisateurs qui ont regulierement besoin d'acceder aux memes enregistrements. Contrairement au partage individuel ou vous accordez l'acces utilisateur par utilisateur, les groupes utilisent des etiquettes intelligentes qui partagent automatiquement le contenu avec tous les membres lorsqu'elles sont appliquees.
|
||||
|
||||
Votre administrateur cree et gere les groupes. Une fois ajoute a un groupe, vous avez acces aux etiquettes de groupe et recevez automatiquement les enregistrements lorsque des collegues appliquent ces etiquettes.
|
||||
|
||||
Les groupes fonctionnent aux cotes de vos outils d'organisation personnels : vos etiquettes personnelles restent privees et independantes, tandis que les etiquettes de groupe creent un contexte partage.
|
||||
|
||||
## Etiquettes de groupe
|
||||
|
||||
Les etiquettes de groupe sont le moteur de la collaboration. Elles ressemblent aux etiquettes regulieres, mais lorsque quelqu'un en applique une a un enregistrement, tous les membres du groupe recoivent automatiquement l'acces.
|
||||
|
||||
### Reconnaitre les etiquettes de groupe
|
||||
|
||||
Les etiquettes de groupe s'affichent avec une icone distinctive d'utilisateurs dans toute l'interface DictIA. Dans votre interface de gestion des etiquettes, les etiquettes de groupe affichent le nom du groupe et ne peuvent pas etre supprimees par les utilisateurs reguliers.
|
||||
|
||||
### Appliquer les etiquettes de groupe
|
||||
|
||||
Appliquer une etiquette de groupe fonctionne exactement comme une etiquette personnelle : ouvrez un enregistrement, cliquez sur le bouton d'etiquettes et selectionnez parmi vos etiquettes disponibles. La difference se fait en coulisse : DictIA cree automatiquement des partages internes pour chaque membre du groupe.
|
||||
|
||||
Le partage automatique accorde aux membres du groupe l'acces en consultation et en edition. Ils peuvent lire les transcriptions et les resumes, ecouter l'audio, modifier les notes et les metadonnees, et ajouter leurs propres etiquettes. Cependant, seul le proprietaire original peut supprimer l'enregistrement.
|
||||
|
||||
!!! info "Partage intelligent"
|
||||
- Si un membre a deja acces par un partage precedent, aucun doublon n'est cree.
|
||||
- Les personnes ajoutees au groupe ulterieurement ne recoivent pas automatiquement acces aux anciens enregistrements, uniquement aux nouveaux.
|
||||
|
||||
### Utiliser les etiquettes de groupe efficacement
|
||||
|
||||
- **Appliquez les etiquettes immediatement** apres les reunions ou discussions importantes pour assurer la visibilite de l'equipe.
|
||||
- **Combinez etiquettes de groupe et personnelles** : utilisez les etiquettes de groupe pour identifier l'audience et vos etiquettes personnelles pour le suivi de votre travail.
|
||||
- **Utilisez plusieurs etiquettes de groupe** lorsque les enregistrements concernent plusieurs equipes, mais assurez-vous que chaque groupe etiquete a reellement besoin d'y acceder.
|
||||
|
||||
### Fonctionnalites avancees des etiquettes de groupe
|
||||
|
||||
Votre administrateur peut configurer des fonctionnalites speciales sur les etiquettes de groupe :
|
||||
|
||||
- **Politiques de retention** : une etiquette « Documents juridiques » pourrait conserver les enregistrements sept ans, tandis qu'une etiquette « Melees quotidiennes » pourrait les supprimer automatiquement apres deux semaines.
|
||||
- **Protection** : les etiquettes protegees empechent toute suppression automatique, quel que soit l'age de l'enregistrement.
|
||||
- **Consignes IA personnalisees** : chaque etiquette peut avoir sa propre consigne de resume. Les revues techniques recoivent des resumes techniques detailles, tandis que les comptes rendus executifs obtiennent des vues d'ensemble de haut niveau.
|
||||
|
||||
## Travailler avec les enregistrements de groupe
|
||||
|
||||
Les enregistrements partages via les etiquettes de groupe apparaissent dans votre interface principale aux cotes de vos propres enregistrements.
|
||||
|
||||
### Identifier le contenu de groupe
|
||||
|
||||
Les cartes d'enregistrement affichent plusieurs indicateurs :
|
||||
|
||||
- **Badge « Groupe »** (bleu) : indique que l'enregistrement a des etiquettes de groupe.
|
||||
- **Badge « Partage »** (violet) : apparait si vous n'etes pas le proprietaire.
|
||||
- **Icone d'utilisateurs** sur les etiquettes : distingue les etiquettes de groupe des etiquettes personnelles.
|
||||
|
||||
Le filtre **Partages avec moi** dans la barre laterale isole les enregistrements partages, y compris ceux partages via les etiquettes de groupe.
|
||||
|
||||
### Vos permissions sur les enregistrements de groupe
|
||||
|
||||
En tant que membre du groupe, vous recevez la permission d'edition sur les enregistrements avec des etiquettes de groupe. Vous pouvez :
|
||||
|
||||
- Modifier le titre, la liste des participants, la date de reunion
|
||||
- Editer les notes et ajouter du contexte
|
||||
- Ajouter des etiquettes pour une meilleure organisation
|
||||
|
||||
Limitations importantes :
|
||||
|
||||
- Vous **ne pouvez pas supprimer** les enregistrements de groupe, meme avec la permission d'edition.
|
||||
- Vous **ne pouvez pas partager** les enregistrements de groupe avec des utilisateurs en dehors du groupe sans permission de repartage.
|
||||
|
||||
### Notes personnelles sur les enregistrements de groupe
|
||||
|
||||
Vous pouvez ajouter des notes personnelles qui restent completement privees. Ces notes ne sont jamais visibles par le proprietaire de l'enregistrement ni par les autres membres du groupe.
|
||||
|
||||
Les notes personnelles persistent tant que vous avez acces a l'enregistrement. Si vous quittez le groupe, vos notes sont automatiquement supprimees.
|
||||
|
||||
## Adhesion au groupe
|
||||
|
||||
### Rejoindre un groupe
|
||||
|
||||
L'adhesion est geree par les administrateurs. Lorsque vous etes ajoute a un groupe :
|
||||
|
||||
- Vous avez immediatement acces aux etiquettes du groupe.
|
||||
- Vous commencez a recevoir les enregistrements lorsque des collegues utilisent les etiquettes de groupe a l'avenir.
|
||||
- Vous n'avez **pas** acces retroactif aux anciens enregistrements etiquetes.
|
||||
|
||||
### Roles dans le groupe
|
||||
|
||||
- **Membres reguliers** : peuvent utiliser les etiquettes de groupe et acceder aux enregistrements etiquetes par d'autres.
|
||||
- **Administrateurs de groupe** : peuvent ajouter ou retirer des membres, creer et modifier des etiquettes de groupe, et gerer la structure organisationnelle du groupe.
|
||||
|
||||
### Quitter un groupe
|
||||
|
||||
Lorsque vous quittez un groupe :
|
||||
|
||||
- Les etiquettes de groupe disparaissent de vos selecteurs.
|
||||
- Votre acces aux enregistrements precedemment partages est conserve, mais vous ne recevrez plus de nouveaux enregistrements.
|
||||
- Vos notes personnelles sur les enregistrements de groupe sont conservees si vous gardez l'acces par d'autres mecanismes de partage.
|
||||
|
||||
## Administration de groupe (pour les administrateurs de groupe)
|
||||
|
||||
Si vous etes administrateur de groupe, vous avez acces a une interface de gestion via le lien **Gestion de groupe** dans votre menu de compte.
|
||||
|
||||
### Gerer les membres
|
||||
|
||||
- Recherchez des utilisateurs par nom, selectionnez leur role initial et ajoutez-les.
|
||||
- Changez les roles entre membre et administrateur.
|
||||
- Le retrait d'un membre coupe l'acces aux futurs enregistrements de groupe.
|
||||
|
||||
### Creer et gerer les etiquettes de groupe
|
||||
|
||||
Les etiquettes de groupe sont le vocabulaire organisationnel de votre equipe. Creez des etiquettes qui correspondent a la facon dont votre equipe pense le contenu : noms de projets, types de contenu, categories de reunions.
|
||||
|
||||
Lors de la creation, vous pouvez definir des periodes de retention personnalisees et des drapeaux de protection selon les besoins du groupe.
|
||||
|
||||
### Synchroniser les partages de groupe
|
||||
|
||||
Si votre groupe a commence a utiliser des etiquettes avant que le partage automatique soit pleinement en place, la fonctionnalite **Synchroniser les partages de groupe** applique retroactivement le partage aux enregistrements deja etiquetes.
|
||||
|
||||
## Integration avec la Recherche IA
|
||||
|
||||
Les etiquettes de groupe apparaissent dans les options de filtrage de la Recherche IA, ce qui vous permet de cibler vos recherches dans le contexte d'un groupe specifique.
|
||||
|
||||
Les enregistrements partages via les etiquettes de groupe sont automatiquement inclus dans vos recherches en Recherche IA, creant une base de connaissances complete qui couvre a la fois vos enregistrements personnels et le contenu de groupe.
|
||||
|
||||
Vos notes personnelles sur les enregistrements de groupe restent privees meme dans la Recherche IA.
|
||||
|
||||
## Bonnes pratiques
|
||||
|
||||
- **Utilisez les etiquettes de groupe des le debut** d'un projet pour batir un historique complet.
|
||||
- **Etablissez des conventions d'equipe** sur quand utiliser les etiquettes de groupe versus le partage individuel.
|
||||
- **Choisissez des noms descriptifs** comme « Planification sprint » ou « Retours clients » plutot que des noms generiques comme « Important ».
|
||||
- **Revisez periodiquement** les etiquettes de groupe avec votre administrateur pour vous assurer qu'elles restent pertinentes.
|
||||
- **Utilisez vos notes personnelles** liberalement pour votre suivi individuel : elles ne sont visibles que par vous.
|
||||
|
||||
---
|
||||
|
||||
Suivant : [Parametres du compte](parametres.md)
|
||||
49
client_docs/guide-utilisateur/index.md
Normal file
49
client_docs/guide-utilisateur/index.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Guide utilisateur
|
||||
|
||||
Bienvenue dans le guide utilisateur de DictIA. Cette section vous accompagne pas a pas dans l'utilisation quotidienne de la plateforme, de la creation de vos premiers enregistrements jusqu'aux fonctionnalites avancees de collaboration et de recherche par intelligence artificielle.
|
||||
|
||||
DictIA transforme vos enregistrements audio en transcriptions intelligentes, enrichies de resumes automatiques, d'identification des locuteurs et de fonctionnalites de recherche semantique.
|
||||
|
||||
---
|
||||
|
||||
## Sections du guide
|
||||
|
||||
<div class="grid cards" markdown>
|
||||
|
||||
- :material-rocket-launch: **[Premiers pas](premiers-pas.md)**
|
||||
|
||||
Decouvrez l'interface de DictIA, la navigation et la disposition des panneaux principaux.
|
||||
|
||||
- :material-microphone: **[Enregistrement et televersement](enregistrement.md)**
|
||||
|
||||
Enregistrez de l'audio directement dans votre navigateur ou televersez des fichiers existants.
|
||||
|
||||
- :material-text-box-outline: **[Transcriptions](transcriptions.md)**
|
||||
|
||||
Consultez, modifiez et exportez vos transcriptions. Utilisez le resume IA, les notes et la conversation IA.
|
||||
|
||||
- :material-magnify: **[Recherche IA](recherche-ia.md)**
|
||||
|
||||
Interrogez l'ensemble de vos enregistrements en langage naturel grace a la recherche semantique.
|
||||
|
||||
- :material-share-variant: **[Partage](partage.md)**
|
||||
|
||||
Partagez vos enregistrements avec des collegues ou creez des liens publics securises.
|
||||
|
||||
- :material-folder-outline: **[Dossiers](dossiers.md)**
|
||||
|
||||
Organisez vos enregistrements dans des dossiers pour une gestion par projet, client ou type de contenu.
|
||||
|
||||
- :material-account-group: **[Groupes](groupes.md)**
|
||||
|
||||
Collaborez en equipe grace aux groupes et aux etiquettes de groupe avec partage automatique.
|
||||
|
||||
- :material-cog: **[Parametres du compte](parametres.md)**
|
||||
|
||||
Personnalisez votre experience : langue, consignes IA, gestion des locuteurs et des etiquettes.
|
||||
|
||||
- :material-cellphone: **[Application mobile](application-mobile.md)**
|
||||
|
||||
Installez DictIA sur votre appareil mobile pour un acces rapide et l'enregistrement en deplacement.
|
||||
|
||||
</div>
|
||||
205
client_docs/guide-utilisateur/parametres.md
Normal file
205
client_docs/guide-utilisateur/parametres.md
Normal file
@@ -0,0 +1,205 @@
|
||||
# Parametres du compte
|
||||
|
||||
Les parametres de votre compte sont le centre de controle pour personnaliser DictIA selon vos besoins. Chaque preference que vous definissez ici facon votre experience quotidienne. Accedez a ces parametres en cliquant sur votre nom d'utilisateur dans la barre de navigation, puis en selectionnant **Compte**.
|
||||
|
||||
## Onglet Informations du compte
|
||||
|
||||
### Informations personnelles
|
||||
|
||||
Mettez a jour votre nom complet, votre titre de poste et votre organisation. Ces informations aident a vous identifier dans les environnements collaboratifs.
|
||||
|
||||
### Preferences linguistiques
|
||||
|
||||
Trois parametres de langue distincts faconnent votre experience :
|
||||
|
||||
- **Langue de l'interface** : transforme tous les menus, boutons et messages dans la langue choisie (francais, anglais, espagnol, chinois ou allemand).
|
||||
- **Langue de transcription** : accepte un code de langue ISO (ex. : « fr » ou « en ») pour optimiser la precision de la reconnaissance. Laissez vide pour activer la detection automatique du contenu multilingue.
|
||||
- **Langue du resume et de la conversation IA** : assure que tout le contenu genere par l'IA apparait dans votre langue preferee, peu importe la langue de l'audio source.
|
||||
|
||||
### Statistiques du compte
|
||||
|
||||
Le panneau de droite affiche vos metriques d'enregistrement :
|
||||
|
||||
- Nombre total d'enregistrements et nombre de traitements termines
|
||||
- Enregistrements en cours de traitement et ceux en erreur
|
||||
|
||||
### Preferences de traitement
|
||||
|
||||
#### Resume automatique
|
||||
|
||||
L'option **Resume automatique** controle si les resumes sont generes automatiquement apres la transcription. Active par defaut. Desactivez-la si vous preferez declencher manuellement la generation de resumes.
|
||||
|
||||
#### Identification automatique des locuteurs
|
||||
|
||||
Lorsque vous avez des profils vocaux avec des empreintes (provenant du service de transcription avancee), l'**Identification automatique des locuteurs** peut reconnaitre et etiqueter les locuteurs dans les nouveaux enregistrements.
|
||||
|
||||
- **Activer/Desactiver** : activez l'identification automatique.
|
||||
- **Seuil de confiance** :
|
||||
- **Bas** : plus de correspondances, mais possibilite de faux positifs.
|
||||
- **Moyen** : precision equilibree (recommande).
|
||||
- **Eleve** : moins de correspondances, mais confiance plus elevee.
|
||||
|
||||
!!! tip "Construire des profils vocaux"
|
||||
L'identification automatique necessite des profils vocaux avec empreintes. Ceux-ci sont crees lorsque vous identifiez des locuteurs dans des enregistrements traites avec le service de transcription avancee. Plus vous identifiez un locuteur dans differents enregistrements, meilleure sera la reconnaissance.
|
||||
|
||||
### Actions du compte
|
||||
|
||||
- **Aller aux enregistrements** : acces direct a votre bibliotheque.
|
||||
- **Changer le mot de passe** : ouvre un dialogue securise pour mettre a jour vos identifiants.
|
||||
- **Gerer les locuteurs** : raccourci vers la gestion des profils de locuteurs.
|
||||
|
||||
## Onglet Consignes personnalisees
|
||||
|
||||
### Votre consigne de resume personnalisee
|
||||
|
||||
Le grand champ de texte accepte des instructions detaillees en langage naturel qui deviennent les directives de l'IA pour generer vos resumes. Pensez-y comme a l'enseignement a un assistant de la facon exacte dont vous souhaitez que vos notes de reunion soient preparees.
|
||||
|
||||
Par exemple, vous pourriez demander des sections specifiques comme « Decisions techniques cles » pour les reunions d'ingenierie ou « Observations sur les patients » pour des consultations medicales.
|
||||
|
||||
### Consigne par defaut en cours
|
||||
|
||||
Sous votre consigne personnalisee, vous pouvez voir la consigne par defaut qui s'applique lorsque vous laissez la votre vide. Cette transparence montre exactement quelles instructions de base l'IA suit.
|
||||
|
||||
### Hierarchie des consignes
|
||||
|
||||
Comprendre l'ordre de priorite vous aide a utiliser cette fonctionnalite efficacement :
|
||||
|
||||
1. **Consignes des etiquettes** (priorite la plus elevee)
|
||||
2. **Votre consigne personnalisee**
|
||||
3. **Consigne par defaut de l'administrateur**
|
||||
4. **Consigne systeme** (dernier recours)
|
||||
|
||||
### Empilage des consignes
|
||||
|
||||
Les consignes de plusieurs etiquettes se combinent intelligemment lorsqu'elles sont appliquees ensemble. Un enregistrement etiquete a la fois « Client » et « Technique » recoit les deux jeux d'instructions.
|
||||
|
||||
### Indications de transcription
|
||||
|
||||
Sous la consigne de resume, deux champs ameliorent la precision de la transcription :
|
||||
|
||||
- **Vocabulaire personnalise par defaut** : liste de mots ou expressions separees par des virgules que le moteur de transcription doit prioriser (ex. : noms de marques, acronymes, terminologie technique).
|
||||
- **Consigne de transcription par defaut** : breve description du contenu typique de vos enregistrements (ex. : « Ceci est une reunion sur le developpement logiciel et les outils d'IA. »).
|
||||
|
||||
Ces valeurs par defaut s'appliquent a tous vos enregistrements, sauf si elles sont remplacees par les parametres d'etiquettes, de dossiers ou les valeurs saisies dans le formulaire de televersement.
|
||||
|
||||
!!! tip "Vocabulaire personnalise vs Consigne de transcription"
|
||||
Utilisez le **vocabulaire personnalise** pour les termes specifiques que le modele a tendance a mal orthographier. Utilisez la **consigne de transcription** pour un contexte plus large qui aide le modele a mieux comprendre le domaine.
|
||||
|
||||
### Rediger des consignes efficaces
|
||||
|
||||
Redigez vos consignes en fonction de la facon dont vous utilisez reellement les resumes. Cherchez-vous a extraire des actions a suivre ? A identifier des decisions strategiques ? A documenter des details techniques ? Votre consigne devrait demander exactement ce dont vous avez besoin.
|
||||
|
||||
## Onglet Transcriptions partagees
|
||||
|
||||
Cet onglet offre une visibilite et un controle complets sur tous les enregistrements que vous avez partages via des liens publics.
|
||||
|
||||
### Informations de partage
|
||||
|
||||
Chaque entree affiche le titre de l'enregistrement, la date de creation du lien, les options selectionnees (resume et/ou notes) et l'URL complete.
|
||||
|
||||
### Gerer les options de partage
|
||||
|
||||
Modifiez a tout moment ce qui est inclus dans un partage en basculant les cases du resume et des notes. Les changements prennent effet immediatement pour toute personne accedant au lien, sans generer de nouveau lien.
|
||||
|
||||
### Revoquer l'acces
|
||||
|
||||
Le bouton de suppression (icone corbeille) revoque instantanement l'acces. Une fois supprime, toute personne tentant d'acceder au lien verra un message d'erreur.
|
||||
|
||||
## Onglet Gestion des locuteurs
|
||||
|
||||
Cet onglet fournit une interface complete pour gerer tous les locuteurs identifies a travers vos enregistrements.
|
||||
|
||||
### Creation automatique des locuteurs
|
||||
|
||||
Les locuteurs sont automatiquement sauvegardes lorsque vous les identifiez lors de l'edition de transcriptions. Le systeme construit votre bibliotheque au fil du temps.
|
||||
|
||||
### Informations sur les fiches de locuteur
|
||||
|
||||
Chaque fiche affiche :
|
||||
|
||||
- Le nom du locuteur
|
||||
- Les statistiques d'utilisation (nombre d'identifications)
|
||||
- La date de derniere utilisation
|
||||
- La date d'ajout
|
||||
|
||||
#### Profil vocal
|
||||
|
||||
Pour les locuteurs avec des donnees de reconnaissance vocale :
|
||||
|
||||
- **Badge de profil vocal** : indique le niveau de confiance (eleve, moyen, bas).
|
||||
- **Nombre d'echantillons** : nombre d'empreintes vocales collectees.
|
||||
- **Bouton d'echantillons vocaux** : ecoutez des extraits representatifs pour verification.
|
||||
- **Indicateur de force du profil** : retour visuel sur la capacite du systeme a reconnaitre ce locuteur.
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Supprimez des locuteurs individuellement avec l'icone corbeille.
|
||||
- Utilisez **Tout supprimer** pour reinitialiser votre base.
|
||||
- Revisez regulierement pour retirer les locuteurs obsoletes et consolider les doublons.
|
||||
|
||||
## Onglet Gestion des etiquettes
|
||||
|
||||
La gestion des etiquettes transforme de simples libelles en instructions de traitement puissantes. Chaque etiquette porte plusieurs capacites : une couleur, une consigne IA optionnelle, des parametres de transcription, des politiques de retention et des configurations de partage.
|
||||
|
||||
### Affichage et indicateurs visuels
|
||||
|
||||
Chaque carte d'etiquette affiche des badges visuels indiquant sa configuration :
|
||||
|
||||
- **Badges de retention** : periode de retention (ex. : « 90 jours ») ou statut de protection (symbole infini)
|
||||
- **Badges de partage** : indique s'il s'agit d'une etiquette de groupe avec partage automatique
|
||||
- **Badges de langue** : parametres de langue personnalises
|
||||
- **Badges de locuteurs** : parametres par defaut (ex. : « Max 10 » locuteurs)
|
||||
- **Consignes personnalisees** : apercu des instructions de resume IA
|
||||
|
||||
### Creer une etiquette
|
||||
|
||||
Cliquez sur **Creer une etiquette**. Le systeme vous guide pour :
|
||||
|
||||
1. Choisir un nom
|
||||
2. Selectionner une couleur
|
||||
3. Optionnellement ajouter une consigne personnalisee
|
||||
4. Definir les parametres par defaut de transcription (nombre de locuteurs attendu, vocabulaire personnalise, consigne de transcription)
|
||||
|
||||
### Empilage des consignes
|
||||
|
||||
Les consignes s'empilent intelligemment lorsque plusieurs etiquettes sont appliquees. Par exemple, etiqueter un enregistrement avec « Reunions BSB » et « Reunions etudiantes » combine les deux consignes pour un resume complet.
|
||||
|
||||
### Vocabulaire personnalise et consigne de transcription par etiquette
|
||||
|
||||
Les etiquettes peuvent avoir un **vocabulaire personnalise** et une **consigne de transcription** par defaut. Par exemple, une etiquette « Rondes medicales » pourrait inclure `epinephrine, tachycardie, intubation` comme vocabulaire et « Discussion d'equipe medicale en contexte de soins intensifs » comme consigne.
|
||||
|
||||
### Gerer les etiquettes
|
||||
|
||||
Les boutons de modification et de suppression sur chaque carte offrent un controle complet. Supprimer une etiquette ne modifie pas les enregistrements deja etiquetes : ils conservent leurs etiquettes pour reference historique.
|
||||
|
||||
## Onglet A propos
|
||||
|
||||
L'onglet A propos presente une vue d'ensemble de votre installation DictIA : version, configuration systeme, fonctionnalites principales et liens vers les ressources.
|
||||
|
||||
### Informations de version
|
||||
|
||||
Le badge de version vous indique quelle version de DictIA vous utilisez, information essentielle pour le depannage.
|
||||
|
||||
### Configuration systeme
|
||||
|
||||
Cette section detaille les capacites de votre instance : le modele d'IA utilise pour les resumes et la conversation IA, ainsi que la configuration de votre service de transcription.
|
||||
|
||||
### Liens utiles
|
||||
|
||||
Des boutons d'acces rapide vous connectent aux ressources essentielles : documentation et support.
|
||||
|
||||
## Securite et confidentialite
|
||||
|
||||
- **Deconnectez-vous** lorsque vous utilisez un ordinateur partage.
|
||||
- **Revisez regulierement** vos enregistrements partages et revoquez l'acces lorsque ce n'est plus necessaire.
|
||||
- **Tenez compte des implications** des parametres de langue dans les contextes internationaux.
|
||||
|
||||
## Optimiser vos parametres
|
||||
|
||||
Commencez par une configuration de base : nom, preferences de langue et une consigne simple. A mesure que vous vous familiarisez avec DictIA, ajoutez des etiquettes, affinez vos consignes et construisez votre bibliotheque de locuteurs.
|
||||
|
||||
Si vous avez des questions ou rencontrez des problemes, contactez le support InnovA AI.
|
||||
|
||||
---
|
||||
|
||||
Retour au [Guide utilisateur](index.md)
|
||||
105
client_docs/guide-utilisateur/partage.md
Normal file
105
client_docs/guide-utilisateur/partage.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# Partage et collaboration
|
||||
|
||||
DictIA offre des options de collaboration flexibles pour s'adapter a votre flux de travail. Partagez des enregistrements en interne avec des collegues, organisez l'acces via des groupes, ou creez des liens publics pour des parties prenantes externes.
|
||||
|
||||
## Partage interne (utilisateur a utilisateur)
|
||||
|
||||
Le partage interne permet la collaboration directe entre les utilisateurs de votre instance DictIA. Le destinataire doit se connecter pour acceder a l'enregistrement et ses permissions sont suivies et applicables.
|
||||
|
||||
### Partager un enregistrement
|
||||
|
||||
Lorsque vous consultez un enregistrement, cliquez sur l'icone de partage (icone d'utilisateurs) dans la barre d'outils. Dans la fenetre de partage :
|
||||
|
||||
1. Recherchez le nom de l'utilisateur (saisissez au moins deux caracteres).
|
||||
2. Choisissez le niveau de permission :
|
||||
|
||||
- **Consultation seule** : le destinataire peut lire la transcription, consulter le resume, ecouter l'audio et utiliser la conversation IA. Il ne peut rien modifier.
|
||||
- **Edition** : en plus de la consultation, le destinataire peut modifier le titre, les participants, la date, le resume partage et les etiquettes. Il ne peut pas supprimer l'enregistrement ni le partager avec d'autres.
|
||||
- **Repartage** : toutes les capacites d'edition, plus la possibilite de partager l'enregistrement avec d'autres utilisateurs et de gerer ces partages. Seul le proprietaire original peut supprimer l'enregistrement.
|
||||
|
||||
3. Cliquez sur le nom de l'utilisateur pour l'ajouter. Le partage prend effet immediatement.
|
||||
|
||||
### Gerer vos partages
|
||||
|
||||
La fenetre de partage affiche toutes les personnes ayant acces a un enregistrement, avec leurs permissions et la date d'octroi. L'icone rouge de suppression permet de revoquer instantanement l'acces.
|
||||
|
||||
La revocation est immediate et complete : le destinataire perd l'acces et ses notes personnelles sur l'enregistrement sont supprimees.
|
||||
|
||||
### Notes personnelles sur les enregistrements partages
|
||||
|
||||
Lorsqu'un enregistrement est partage avec vous, vous pouvez ajouter vos propres notes privees, invisibles pour le proprietaire et les autres destinataires.
|
||||
|
||||
!!! info "Notes vs Resume"
|
||||
- **Notes** : toujours personnelles et privees. Chaque utilisateur peut creer ses propres notes sans permission speciale.
|
||||
- **Resume** : partage globalement. Visible par tous les utilisateurs ayant acces. Seuls ceux avec la permission d'edition peuvent le modifier.
|
||||
|
||||
### Boite de reception et favoris independants
|
||||
|
||||
Chaque utilisateur maintient son propre etat de boite de reception et de favoris pour les enregistrements partages. Marquer un enregistrement comme lu ou en favori n'affecte pas les autres utilisateurs.
|
||||
|
||||
### Voir les enregistrements partages avec vous
|
||||
|
||||
Les enregistrements partages apparaissent dans votre liste principale avec des indicateurs visuels distinctifs (titre colore et bordure subtile). Utilisez le filtre **Partages avec moi** dans la barre laterale pour voir uniquement ces enregistrements.
|
||||
|
||||
## Liens de partage publics
|
||||
|
||||
Vous pouvez creer des liens securises pour partager un enregistrement avec des personnes qui n'ont pas de compte DictIA.
|
||||
|
||||
### Creer un lien de partage
|
||||
|
||||
En consultant un enregistrement, cliquez sur le bouton de partage dans la barre d'outils. Deux options vous sont proposees :
|
||||
|
||||
- **Partager le resume** : permet aux destinataires de voir le resume genere par l'IA.
|
||||
- **Partager les notes** : permet aux destinataires de voir vos notes personnelles.
|
||||
|
||||
La transcription est toujours incluse comme contenu principal.
|
||||
|
||||
Cliquez sur **Creer un lien de partage** pour generer l'URL securisee. Le lien est permanent jusqu'a sa revocation explicite.
|
||||
|
||||
### Ce que les destinataires voient
|
||||
|
||||
Les destinataires accedent a une vue epuree et professionnelle de votre enregistrement :
|
||||
|
||||
- Le titre de l'enregistrement
|
||||
- Le lecteur audio avec controles de lecture complets
|
||||
- La transcription avec les etiquettes de locuteurs et les options d'affichage (vue simple et a bulles)
|
||||
- Le resume et/ou les notes (selon vos choix)
|
||||
- Un bouton de copie pour extraire facilement le texte
|
||||
|
||||
L'experience est en lecture seule : les destinataires ne peuvent rien modifier et n'ont acces a aucune autre partie de votre compte.
|
||||
|
||||
### Gerer vos liens de partage
|
||||
|
||||
Tous vos liens de partage sont centralises dans l'onglet **Transcriptions partagees** de vos [parametres du compte](parametres.md). Vous pouvez :
|
||||
|
||||
- Modifier les options de partage (resume et/ou notes) sans generer de nouveaux liens.
|
||||
- Copier a nouveau l'URL si necessaire.
|
||||
- Supprimer un partage pour revoquer instantanement l'acces.
|
||||
|
||||
## Scenarios pratiques
|
||||
|
||||
| Scenario | Approche | Pourquoi |
|
||||
|----------|----------|----------|
|
||||
| Enregistrements familiaux | Creer un groupe « Famille », etiqueter les evenements | Acces automatique pour tous les membres |
|
||||
| Club de lecture | Creer un groupe avec etiquette protegee | Enregistrements preserves, partage automatique |
|
||||
| Projet temporaire a 3 personnes | Partage individuel avec edition | Collaboration temporaire, facile a revoquer |
|
||||
| Reunions de departement | Creer un groupe, etiqueter les reunions | Nouveaux membres recoivent les futures reunions |
|
||||
| Presentations clients | Partage individuel en consultation seule | Acces controle, pas de modification accidentelle |
|
||||
|
||||
## Combiner les approches
|
||||
|
||||
Utilisez les groupes pour la collaboration reguliere et le partage individuel pour les exceptions. Votre equipe a un acces automatique aux discussions via les etiquettes de groupe. Quand une decision necessite un examen par la direction, partagez cet enregistrement specifique individuellement.
|
||||
|
||||
## Bonnes pratiques
|
||||
|
||||
- **Partagez rapidement** apres les reunions pendant que le contexte est frais.
|
||||
- **Utilisez la consultation seule par defaut** et n'accordez l'edition que lorsque les destinataires doivent vraiment contribuer.
|
||||
- **Revisez periodiquement** vos enregistrements partages pour vous assurer que l'acces reste approprie.
|
||||
- **Traitez les liens de partage** avec le meme soin que des documents sensibles : partagez-les uniquement par des canaux securises.
|
||||
|
||||
!!! warning "Rappel important"
|
||||
Les liens de partage ne necessitent pas d'authentification. Toute personne disposant du lien peut acceder a l'enregistrement. Revoquer un lien empeche tout acces futur, mais ne peut pas recuperer l'information deja consultee ou telechargee.
|
||||
|
||||
---
|
||||
|
||||
Suivant : [Dossiers](dossiers.md)
|
||||
123
client_docs/guide-utilisateur/premiers-pas.md
Normal file
123
client_docs/guide-utilisateur/premiers-pas.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# Premiers pas
|
||||
|
||||
L'interface principale de DictIA est l'endroit ou vous passerez la majorite de votre temps. Elle est concue en trois panneaux qui facilitent la navigation entre vos enregistrements, la lecture des transcriptions et l'interaction avec votre contenu.
|
||||
|
||||
## Comprendre la disposition de l'interface
|
||||
|
||||
L'interface principale est organisee en trois sections qui fonctionnent ensemble.
|
||||
|
||||
- **Panneau de gauche** : la barre laterale liste tous vos enregistrements audio avec des options de filtrage et de recherche.
|
||||
- **Panneau central** : affiche la transcription complete de l'enregistrement selectionne, avec identification des locuteurs si disponible.
|
||||
- **Panneau de droite** : contient le lecteur audio, le resume genere par l'IA, vos notes personnelles et la conversation IA.
|
||||
|
||||
Au-dessus des panneaux central et droit, une barre de metadonnees affiche les informations de l'enregistrement et les boutons d'action.
|
||||
|
||||
## Barre de navigation superieure
|
||||
|
||||
La barre de navigation en haut de l'ecran offre un acces rapide aux fonctionnalites essentielles :
|
||||
|
||||
- **Logo DictIA** : vous ramene a la vue principale depuis n'importe quelle page.
|
||||
- **Bouton Recherche IA** : ouvre l'interface de recherche semantique pour interroger l'ensemble de vos enregistrements en langage naturel.
|
||||
- **Bouton Nouvel enregistrement** : permet de televerser un fichier audio ou d'enregistrer directement dans votre navigateur.
|
||||
- **Menu utilisateur** (a droite) : acces aux parametres du compte, aux preferences de langue et a la deconnexion.
|
||||
|
||||
## Barre laterale gauche — Liste et filtres
|
||||
|
||||
La barre laterale est votre centre de commande pour organiser et retrouver vos enregistrements.
|
||||
|
||||
### Barre de recherche
|
||||
|
||||
En haut de la barre laterale, un champ de recherche vous permet de retrouver rapidement un enregistrement par son titre ou son contenu.
|
||||
|
||||
### Utiliser les filtres
|
||||
|
||||
Cliquez sur **Filtres actifs** pour deployer le panneau de filtrage. Vous pouvez filtrer par :
|
||||
|
||||
- **Etiquettes** : selectionnez ou deselectionnez les etiquettes affichees sous forme de pastilles colorees.
|
||||
- **Plage de dates** : utilisez les raccourcis (Aujourd'hui, Hier, Cette semaine, Semaine derniere) ou definissez une plage personnalisee.
|
||||
- **Partages** : activez le filtre **Partages avec moi** pour voir uniquement les enregistrements partages par d'autres utilisateurs.
|
||||
|
||||
Lorsque des filtres sont actifs, leur nombre s'affiche a cote du libelle. Un bouton permet de reinitialiser tous les filtres d'un coup.
|
||||
|
||||
### Cartes d'enregistrement
|
||||
|
||||
Chaque enregistrement s'affiche sous forme de carte montrant :
|
||||
|
||||
- Le titre
|
||||
- Les participants (si l'identification des locuteurs a ete utilisee)
|
||||
- La date et la duree
|
||||
- Les etiquettes sous forme de pastilles colorees
|
||||
- Des icones indiquant l'etat du traitement (termine, en cours ou en erreur)
|
||||
|
||||
## Panneau central — Vue de la transcription
|
||||
|
||||
Le panneau central affiche le texte complet de votre enregistrement. Si l'identification des locuteurs est activee, chaque intervention est etiquetee avec un indicateur de couleur (LOCUTEUR_01, LOCUTEUR_02, etc.). Vous pouvez cliquer sur n'importe quelle phrase pour positionner le lecteur audio a cet endroit precis.
|
||||
|
||||
### Options d'affichage
|
||||
|
||||
- **Bouton Copier** : copie la transcription complete dans le presse-papier.
|
||||
- **Bouton Modifier** : active l'edition en ligne pour corriger la transcription.
|
||||
- **Bascule Simple / Bulles** : alterne entre un affichage en texte continu et un format de conversation a bulles (disponible uniquement avec l'identification des locuteurs).
|
||||
|
||||
## Barre de metadonnees de l'enregistrement
|
||||
|
||||
Cette barre affiche le titre, les participants, la date, la taille du fichier et la duree. Les boutons d'action a droite permettent de :
|
||||
|
||||
- **Marquer en favori** (icone etoile)
|
||||
- **Modifier les details** (icone crayon)
|
||||
- **Gerer les etiquettes** (icone etiquette)
|
||||
- **Retraiter la transcription** (icone rafraichir)
|
||||
- **Partager** (icone partage)
|
||||
- **Supprimer** (icone corbeille)
|
||||
|
||||
## Panneau de droite — Lecture, resume, notes et conversation IA
|
||||
|
||||
### Controles de lecture
|
||||
|
||||
Le lecteur audio en haut du panneau de droite vous permet de lire, mettre en pause et naviguer dans l'enregistrement. Vous pouvez ajuster la vitesse de lecture de 0,5x a 2x. Si l'identification des locuteurs est activee, cliquer sur une phrase dans la transcription positionne automatiquement le lecteur a ce moment precis.
|
||||
|
||||
### Onglet Resume
|
||||
|
||||
L'onglet **Resume** affiche une vue d'ensemble generee par l'IA : points cles, decisions prises et actions a suivre. Le resume est cree automatiquement apres la transcription et peut etre regenere avec differents parametres.
|
||||
|
||||
### Onglet Notes
|
||||
|
||||
L'onglet **Notes** est votre espace personnel pour ajouter du contexte, des reflexions ou des suivis lies a l'enregistrement. Ces notes sont privees et consultables uniquement par vous. Elles prennent en charge le format Markdown.
|
||||
|
||||
### Onglet Conversation IA
|
||||
|
||||
L'onglet **Conversation IA** met a votre disposition un assistant qui peut repondre a vos questions sur l'enregistrement. Posez des questions comme « Quelles sont les decisions prises ? » ou « Qu'a dit Marie a propos du budget ? » et l'IA analysera la transcription pour vous fournir des reponses pertinentes.
|
||||
|
||||
## Options de retraitement
|
||||
|
||||
DictIA offre deux types de retraitement pour mettre a jour vos enregistrements :
|
||||
|
||||
### Retraitement complet
|
||||
|
||||
Le retraitement complet retranscrit entierement votre fichier audio. Ceci est utile lorsque vous souhaitez modifier les parametres d'identification des locuteurs ou utiliser un modele de transcription different. **Attention** : le retraitement complet ecrase les modifications manuelles apportees a la transcription.
|
||||
|
||||
### Retraitement du resume
|
||||
|
||||
Le retraitement du resume genere un nouveau titre et un nouveau resume a partir de la transcription existante, sans modifier le texte transcrit. Cette option est plus rapide et preserve vos corrections manuelles.
|
||||
|
||||
## Travailler avec plusieurs enregistrements
|
||||
|
||||
Vous pouvez combiner plusieurs filtres pour retrouver exactement ce que vous cherchez. Par exemple, filtrer par l'etiquette « Reunion » et la semaine derniere, puis rechercher « budget » dans le contenu.
|
||||
|
||||
Les options de tri permettent d'organiser vos enregistrements par date de creation (televersement) ou par date de reunion (date reelle de l'enregistrement).
|
||||
|
||||
## Conseils pour une utilisation efficace
|
||||
|
||||
- **Adoptez une strategie d'etiquetage coherente** des le depart. Creez des etiquettes par projet, client ou type de reunion.
|
||||
- **Utilisez la recherche avec des termes precis** : noms de projets, sujets specifiques ou termes techniques.
|
||||
- **Prenez des notes immediatement** apres vos reunions pendant que le contexte est frais.
|
||||
- **Pour les longs enregistrements**, utilisez la conversation IA pour poser des questions specifiques plutot que de relire la transcription en entier.
|
||||
- L'interface se met a jour en temps reel : pas besoin de recharger la page apres un televersement ou un traitement.
|
||||
|
||||
## Fonctionnalites de collaboration
|
||||
|
||||
Si vous travaillez en equipe, les fonctionnalites de collaboration de DictIA permettent de partager automatiquement les enregistrements avec les bonnes personnes. Consultez les sections [Partage](partage.md) et [Groupes](groupes.md) pour en savoir plus.
|
||||
|
||||
---
|
||||
|
||||
Suivant : [Enregistrement et televersement](enregistrement.md)
|
||||
80
client_docs/guide-utilisateur/recherche-ia.md
Normal file
80
client_docs/guide-utilisateur/recherche-ia.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# Recherche IA
|
||||
|
||||
La Recherche IA transforme l'ensemble de votre bibliotheque d'enregistrements en une base de connaissances intelligente que vous pouvez interroger en langage naturel. Plutot que de chercher dans chaque enregistrement individuellement, posez simplement vos questions et recevez des reponses completes tirees de tous vos enregistrements pertinents.
|
||||
|
||||
## Comprendre la Recherche IA
|
||||
|
||||
La Recherche IA fonctionne comme un assistant qui a ecoute chacun de vos enregistrements et peut instantanement retrouver et synthetiser l'information de n'importe lequel d'entre eux.
|
||||
|
||||
Cette capacite de recherche semantique va bien au-dela de la simple correspondance de mots cles. Le systeme comprend les concepts, les relations et le contexte. Si vous demandez « preoccupations budgetaires », il trouvera les discussions sur les contraintes financieres, les depassements de couts ou les problemes de financement, meme si l'expression exacte « preoccupations budgetaires » n'a jamais ete prononcee.
|
||||
|
||||
## Acceder a la Recherche IA
|
||||
|
||||
Cliquez sur le bouton **Recherche IA** dans la barre de navigation superieure. L'interface s'ouvre avec une zone de recherche en haut et des options de filtrage sur le cote gauche.
|
||||
|
||||
La beaute de la Recherche IA reside dans sa simplicite : tapez votre question comme vous la poseriez a un collegue, et le systeme se charge de la complexite.
|
||||
|
||||
## Utiliser les filtres pour cibler votre recherche
|
||||
|
||||
La barre laterale gauche contient des filtres qui vous aident a circonscrire votre recherche avant de poser votre question :
|
||||
|
||||
- **Filtre par etiquettes** : selectionnez les enregistrements avec des etiquettes specifiques. Ideal pour chercher au sein d'un projet ou d'un type de reunion particulier.
|
||||
- **Filtre par locuteur** : concentrez-vous sur ce qu'une personne specifique a dit a travers tous les enregistrements. Tres utile pour suivre les engagements d'une personne ou comprendre sa perspective.
|
||||
- **Filtre par plage de dates** : ciblez des discussions recentes ou une periode specifique. Utile pour suivre l'evolution des discussions au fil du temps.
|
||||
|
||||
## Poser des questions efficaces
|
||||
|
||||
La cle pour obtenir d'excellents resultats est de poser des questions claires et specifiques avec du contexte.
|
||||
|
||||
Au lieu de chercher un mot isole comme « echeance », posez une question complete : **« Quelles sont les echeances a venir pour le projet de refonte du site web ? »**
|
||||
|
||||
Le systeme excelle avec differents types de requetes :
|
||||
|
||||
- **Syntheses thematiques** : « Qu'a-t-on discute au sujet de la migration vers le nuage au cours du dernier mois ? »
|
||||
- **Listes d'actions** : « Quelles sont les actions a suivre assignees lors des reunions de cette semaine ? »
|
||||
- **Recherches specifiques** : « Quels engagements ont ete pris envers le client Acme ? »
|
||||
- **Analyses de tendances** : « Comment les priorites du projet ont-elles evolue au cours du dernier trimestre ? »
|
||||
- **Identification de problemes** : « Quels risques ont ete identifies pour le lancement du produit ? »
|
||||
|
||||
## Comprendre vos resultats
|
||||
|
||||
Les resultats de la Recherche IA ne sont pas une simple liste de correspondances : il s'agit d'une reponse coherente qui combine l'information provenant de plusieurs enregistrements en une synthese unifiee.
|
||||
|
||||
Chaque element d'information inclut une citation au format **(Enregistrement ID : XX)** qui renvoie directement a l'enregistrement source. Cliquez sur ces liens pour acceder au contexte complet et verifier l'information.
|
||||
|
||||
Le systeme organise intelligemment l'information en regroupant les points connexes et en les presentant dans un flux logique. Si plusieurs enregistrements traitent du meme sujet, la reponse synthetise ces discussions en montrant l'evolution de la conversation au fil du temps.
|
||||
|
||||
## Applications pratiques
|
||||
|
||||
### Preparation de reunions
|
||||
|
||||
Passez en revue rapidement ce qui a ete discute precedemment sur les sujets a l'ordre du jour, les questions restees sans reponse et les engagements pris.
|
||||
|
||||
### Gestion de projets
|
||||
|
||||
Suivez les decisions prises lors de reunions multiples, identifiez les actions a suivre assignees aux membres de l'equipe et surveillez l'evolution des exigences.
|
||||
|
||||
### Recherche et analyse
|
||||
|
||||
Analysez les tendances dans les commentaires clients, suivez les mentions de concurrents ou identifiez les themes recurrents dans vos entrevues.
|
||||
|
||||
### Conformite et documentation
|
||||
|
||||
Retrouvez rapidement les engagements pris envers des clients, localisez les discussions sur les exigences reglementaires ou verifiez ce qui a ete convenu lors de negociations.
|
||||
|
||||
## Strategies de recherche avancees
|
||||
|
||||
- **Commencez par des questions larges** pour comprendre le paysage de l'information disponible, puis affinez avec des questions de plus en plus specifiques.
|
||||
- **Combinez les filtres strategiquement** : pour preparer une reunion client, filtrez par l'etiquette du client et la plage de dates pertinente, puis demandez les enjeux en suspens.
|
||||
- **Utilisez la terminologie de vos discussions** : le systeme apprend des modeles dans vos enregistrements, donc utiliser un vocabulaire coherent donne de meilleurs resultats.
|
||||
|
||||
## Limites a garder en tete
|
||||
|
||||
- La Recherche IA interroge uniquement le texte transcrit, pas l'audio directement. Les erreurs de transcription peuvent affecter les resultats.
|
||||
- Le systeme ne peut pas inferer l'information qui n'a pas ete explicitement dite dans les enregistrements.
|
||||
- Les enregistrements doivent etre entierement traites avant d'etre interrogeables ; les enregistrements tres recents pourraient ne pas apparaitre immediatement.
|
||||
- La prise en charge linguistique est optimisee pour le francais et l'anglais. Les autres langues peuvent fonctionner avec des resultats variables.
|
||||
|
||||
---
|
||||
|
||||
Suivant : [Partage](partage.md)
|
||||
184
client_docs/guide-utilisateur/transcriptions.md
Normal file
184
client_docs/guide-utilisateur/transcriptions.md
Normal file
@@ -0,0 +1,184 @@
|
||||
# Travailler avec les transcriptions
|
||||
|
||||
Une fois votre audio traite, DictIA met a votre disposition un ensemble d'outils pour consulter, modifier et interagir avec vos transcriptions.
|
||||
|
||||
## Vue de la transcription
|
||||
|
||||
Lorsque vous selectionnez un enregistrement dans la barre laterale, le panneau central affiche la transcription complete avec des indicateurs visuels pour les differents locuteurs.
|
||||
|
||||
## Identification des locuteurs
|
||||
|
||||
Si votre enregistrement a ete traite avec l'identification des locuteurs activee, chaque intervention est etiquetee avec des indicateurs colores (LOCUTEUR_01, LOCUTEUR_02, etc.).
|
||||
|
||||
### Identifier les locuteurs
|
||||
|
||||
Pour attribuer de vrais noms aux etiquettes generiques, cliquez sur le bouton d'identification des locuteurs dans la barre d'outils. Une fenetre s'ouvre ou vous pouvez :
|
||||
|
||||
- Voir un echantillon du dialogue de chaque locuteur detecte pour vous aider a les identifier.
|
||||
- Beneficier de suggestions intelligentes basees sur les profils vocaux de vos enregistrements precedents, avec des scores de confiance.
|
||||
- Saisir les vrais noms pour chaque locuteur : ils remplaceront les etiquettes generiques dans toute la transcription.
|
||||
|
||||
Le systeme memorise ces attributions et construit des profils vocaux, rendant l'identification plus rapide et plus precise dans les enregistrements suivants.
|
||||
|
||||
### Gerer les locuteurs enregistres
|
||||
|
||||
Tous les locuteurs que vous identifiez sont automatiquement sauvegardes dans votre base de locuteurs. Vous pouvez les gerer depuis l'onglet **Gestion des locuteurs** dans vos [parametres du compte](parametres.md).
|
||||
|
||||
Chaque fiche de locuteur affiche le nom, les statistiques d'utilisation, la date de derniere utilisation et la date d'ajout. Vous pouvez supprimer des locuteurs individuellement ou utiliser le bouton **Tout supprimer** pour reinitialiser votre base.
|
||||
|
||||
## Modifier les transcriptions
|
||||
|
||||
DictIA propose deux modes d'edition selon le type de transcription.
|
||||
|
||||
### Edition de texte simple
|
||||
|
||||
Pour les transcriptions sans identification des locuteurs, cliquez sur **Modifier** dans la barre d'outils pour acceder a un editeur de texte simple. Vous pouvez corriger les erreurs, ajouter de la ponctuation et ameliorer le formatage. Vos modifications sont conservees meme lors de la regeneration des resumes.
|
||||
|
||||
### Edition avancee par segments
|
||||
|
||||
Pour les transcriptions avec identification des locuteurs, DictIA offre un editeur par segments qui preserve la structure et le minutage.
|
||||
|
||||
Pour chaque segment, vous pouvez :
|
||||
|
||||
- **Modifier le nom du locuteur** : cliquez sur le champ du locuteur pour le changer, avec des suggestions intelligentes depuis votre base de locuteurs.
|
||||
- **Ajuster les horodatages** : affinez les heures de debut et de fin de chaque segment.
|
||||
- **Modifier le texte** : corrigez les erreurs de transcription directement dans le champ texte.
|
||||
- **Gerer les segments** : supprimez les segments indesirables ou ajoutez-en de nouveaux.
|
||||
|
||||
Vos modifications sont sauvegardees lorsque vous cliquez sur **Enregistrer les modifications**.
|
||||
|
||||
!!! warning "Retraitement complet"
|
||||
Si vous lancez un retraitement complet de la transcription, vos modifications manuelles seront ecrasees. Pour conserver vos corrections, utilisez plutot le retraitement du resume uniquement.
|
||||
|
||||
## Options d'affichage
|
||||
|
||||
### Vue simple
|
||||
|
||||
La vue par defaut presente la transcription sous forme de texte continu avec des etiquettes de locuteurs en ligne. Ideale pour la lecture rapide et la copie de texte.
|
||||
|
||||
### Vue a bulles
|
||||
|
||||
La vue a bulles formate la transcription comme une conversation avec des bulles de message distinctes pour chaque locuteur. Particulierement utile pour les entrevues, debats ou tout echange rapide.
|
||||
|
||||
## Synchronisation audio et mode suivi
|
||||
|
||||
Pour les enregistrements avec identification des locuteurs, DictIA offre une synchronisation bidirectionnelle entre le lecteur audio et la transcription.
|
||||
|
||||
### Navigation par clic
|
||||
|
||||
Cliquez sur n'importe quelle partie de la transcription pour acceder directement a ce moment dans l'audio. Fonctionne dans les deux modes d'affichage.
|
||||
|
||||
### Mise en evidence du segment actif
|
||||
|
||||
Pendant la lecture audio, DictIA met automatiquement en evidence le texte actuellement prononce avec un encadre arrondi et une animation subtile.
|
||||
|
||||
### Mode suivi avec defilement automatique
|
||||
|
||||
Activez le mode suivi en cochant la case dans l'en-tete de la transcription (icone de fleches verticales). La transcription defilera automatiquement pour garder le segment actif au centre de la vue. Particulierement utile pour :
|
||||
|
||||
- Suivre de longs enregistrements sans defilement manuel
|
||||
- Reviser des sections specifiques a differentes vitesses de lecture
|
||||
- Maintenir la synchronisation entre le visuel et l'audio
|
||||
|
||||
Votre preference de mode suivi est sauvegardee automatiquement et persiste entre les sessions.
|
||||
|
||||
## Utiliser le resume
|
||||
|
||||
L'onglet **Resume** dans le panneau de droite contient une vue d'ensemble generee par l'IA. Le resume capture typiquement :
|
||||
|
||||
- Les principaux sujets abordes
|
||||
- Les decisions ou conclusions prises
|
||||
- Les actions a suivre et les responsables
|
||||
- Les dates ou echeances importantes mentionnees
|
||||
- Les problemes ou preoccupations souleves
|
||||
|
||||
### Personnaliser les consignes de resume
|
||||
|
||||
DictIA permet de personnaliser la generation du resume a trois niveaux :
|
||||
|
||||
1. **Niveau administrateur** : consigne par defaut pour tous les utilisateurs.
|
||||
2. **Niveau utilisateur** : votre consigne personnelle dans les [parametres du compte](parametres.md), qui remplace celle de l'administrateur.
|
||||
3. **Niveau etiquette** : chaque etiquette peut avoir sa propre consigne. Par exemple, une etiquette « Reunion juridique » pourrait se concentrer sur la conformite, tandis qu'une etiquette « Planification produit » mettrait l'accent sur les fonctionnalites et les echeances.
|
||||
|
||||
### Ordre de priorite des consignes
|
||||
|
||||
1. **Consignes des etiquettes** (priorite la plus elevee) : si l'enregistrement a des etiquettes avec des consignes, celles-ci priment. Plusieurs etiquettes combinent leurs consignes intelligemment.
|
||||
2. **Consigne utilisateur** : appliquee en l'absence de consignes d'etiquettes.
|
||||
3. **Consigne administrateur** : utilisee lorsque aucune consigne utilisateur n'est configuree.
|
||||
4. **Consigne systeme** : dernier recours si aucune consigne n'existe a aucun niveau.
|
||||
|
||||
### Modifier et exporter les resumes
|
||||
|
||||
Le resume est entierement modifiable avec un editeur Markdown. Cliquez sur l'icone crayon pour entrer en mode edition. Les modifications sont enregistrees automatiquement.
|
||||
|
||||
Vous pouvez exporter le resume de deux facons :
|
||||
|
||||
- **Copier** : copie le resume au format Markdown dans le presse-papier.
|
||||
- **Telecharger** : exporte le resume en fichier Microsoft Word (.docx).
|
||||
|
||||
### Retraiter le resume avec des consignes differentes
|
||||
|
||||
Cliquez sur le bouton de retraitement dans la barre d'outils du resume pour regenerer le resume. Trois options sont disponibles :
|
||||
|
||||
- **Consigne par defaut** : applique la hierarchie de consignes standard.
|
||||
- **Consigne d'une etiquette** : selectionnez n'importe quelle etiquette pour appliquer sa consigne, meme si l'enregistrement n'a pas cette etiquette. Ideal pour experimenter.
|
||||
- **Consigne personnalisee** : saisissez une consigne unique pour cette seule regeneration (ex. : « Extraire tous les termes techniques et leurs definitions »).
|
||||
|
||||
Le retraitement du resume ne modifie jamais votre transcription, y compris vos corrections manuelles.
|
||||
|
||||
## Extraction d'evenements
|
||||
|
||||
Lorsque l'extraction d'evenements est activee dans vos parametres du compte, DictIA identifie automatiquement les evenements dignes d'etre ajoutes a votre calendrier : reunions, echeances, rendez-vous et autres elements sensibles au temps.
|
||||
|
||||
### Consulter les evenements extraits
|
||||
|
||||
Apres le traitement, un onglet **Evenements** apparait dans le panneau de droite si des evenements ont ete detectes. Chaque evenement affiche le titre, la date, l'heure et une description extraite de la conversation.
|
||||
|
||||
### Exporter vers le calendrier
|
||||
|
||||
Chaque evenement peut etre exporte en fichier ICS compatible avec Google Agenda, Outlook, Apple Calendar et Thunderbird. Cliquez sur le bouton de telechargement a cote de l'evenement. Lorsque aucune heure precise n'est mentionnee, les evenements sont programmes a 9 h par defaut.
|
||||
|
||||
## Conversation IA
|
||||
|
||||
L'onglet **Conversation IA** met a votre disposition un assistant qui peut repondre a des questions sur votre enregistrement. Particulierement utile pour les longs enregistrements ou la recherche d'informations specifiques prendrait beaucoup de temps.
|
||||
|
||||
### Poser des questions efficaces
|
||||
|
||||
Vous pouvez poser differents types de questions :
|
||||
|
||||
- Questions factuelles : « Qu'a dit Marie a propos du budget ? »
|
||||
- Questions d'analyse : « Quelles sont les principales preoccupations soulevees ? »
|
||||
- Questions de synthese : « Resume les actions a suivre pour l'equipe marketing. »
|
||||
|
||||
La conversation IA maintient le contexte, vous pouvez donc poser des questions de suivi.
|
||||
|
||||
### Exporter les conversations IA
|
||||
|
||||
- **Copier dans le presse-papier** : copie la conversation complete au format question-reponse.
|
||||
- **Telecharger en Word** : exporte l'historique complet en fichier Microsoft Word (.docx).
|
||||
|
||||
## Notes personnelles
|
||||
|
||||
L'onglet **Notes** est votre espace personnel et prive pour ajouter du contexte et des reflexions sur l'enregistrement.
|
||||
|
||||
!!! info "Notes vs Resume"
|
||||
- **Notes** : toujours privees et visibles uniquement par vous, meme sur les enregistrements partages. Aucune permission speciale n'est requise pour ajouter des notes.
|
||||
- **Resume** : contenu partage visible par tous les utilisateurs ayant acces a l'enregistrement. Seuls les utilisateurs avec la permission d'edition peuvent modifier le resume.
|
||||
|
||||
Les notes supportent le format Markdown complet et sont entierement consultables via la recherche, aux cotes des transcriptions. Elles sont enregistrees automatiquement au fur et a mesure de la saisie.
|
||||
|
||||
## Copier et exporter
|
||||
|
||||
### Copier la transcription
|
||||
|
||||
Le bouton **Copier** dans la barre d'outils copie la transcription complete en texte brut dans le presse-papier, en conservant la structure des paragraphes.
|
||||
|
||||
### Telecharger la transcription
|
||||
|
||||
Le bouton **Telecharger** offre des options d'exportation flexibles grace a des modeles personnalisables. Selectionnez parmi vos modeles de transcription enregistres ou exportez la transcription brute. Les modeles vous permettent de controler le formatage : horodatages pour les sous-titres, format centre sur les locuteurs pour les entrevues, etc.
|
||||
|
||||
Si l'identification des locuteurs est activee, le texte exporte inclut les noms des locuteurs (generiques ou reels si vous les avez identifies).
|
||||
|
||||
---
|
||||
|
||||
Suivant : [Recherche IA](recherche-ia.md)
|
||||
48
client_docs/index.md
Normal file
48
client_docs/index.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# Bienvenue sur DictIA
|
||||
|
||||
DictIA est une plateforme de transcription intelligente propulsée par l'intelligence artificielle, développée par **InnovA AI**. Elle transforme vos enregistrements audio en contenu structuré, consultable et exploitable : transcriptions, résumés automatiques, identification des locuteurs et recherche sémantique.
|
||||
|
||||
---
|
||||
|
||||
## Sections de la documentation
|
||||
|
||||
<div class="grid cards">
|
||||
<div class="card">
|
||||
<h3>Guide Utilisateur</h3>
|
||||
<p>Apprenez à enregistrer, transcrire et organiser vos contenus audio avec DictIA.</p>
|
||||
<a href="guide-utilisateur/" class="card-link">Consulter le guide utilisateur</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Guide Administrateur</h3>
|
||||
<p>Configurez les utilisateurs, les groupes, les modèles IA et les paramètres système de votre instance DictIA.</p>
|
||||
<a href="guide-admin/" class="card-link">Consulter le guide administrateur</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Dépannage</h3>
|
||||
<p>Résolvez les problèmes courants liés à la transcription, à la performance et aux fonctionnalités.</p>
|
||||
<a href="depannage/" class="card-link">Consulter le dépannage</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Fonctionnalités principales
|
||||
|
||||
- **Transcription IA** : Convertissez vos enregistrements audio en texte avec une précision élevée, dans plusieurs langues.
|
||||
- **Résumés automatiques** : Obtenez un résumé structuré de chaque enregistrement, avec les points clés et les actions à suivre.
|
||||
- **Identification des locuteurs** : Distinguez automatiquement les différents intervenants dans un enregistrement.
|
||||
- **Recherche IA** : Posez des questions en langage naturel pour retrouver l'information à travers tous vos enregistrements.
|
||||
- **Conversation IA** : Interagissez avec vos transcriptions pour extraire des informations, générer des rapports ou poser des questions.
|
||||
- **Organisation flexible** : Classez vos enregistrements avec des étiquettes, des dossiers et des groupes.
|
||||
- **Partage sécurisé** : Partagez des enregistrements avec vos collègues ou via des liens externes sécurisés.
|
||||
- **Application mobile** : Accédez à DictIA depuis votre appareil mobile grâce à l'application web progressive.
|
||||
|
||||
---
|
||||
|
||||
## Besoin d'aide?
|
||||
|
||||
Si vous rencontrez un problème ou avez une question, consultez la section [Dépannage](depannage/index.md) ou contactez le support InnovA AI.
|
||||
|
||||
**Support InnovA AI** : Pour toute question technique ou demande d'assistance, contactez l'équipe InnovA AI via les canaux de support mis à votre disposition par votre administrateur.
|
||||
65
config/docker-compose.example.yml
Normal file
65
config/docker-compose.example.yml
Normal file
@@ -0,0 +1,65 @@
|
||||
services:
|
||||
app:
|
||||
# Use 'lite' tag for a smaller image (~700MB vs ~4.4GB) without PyTorch
|
||||
# Semantic search in Inquire Mode falls back to text search; all other features work normally
|
||||
image: dictia:latest
|
||||
container_name: dictia
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8899:8899"
|
||||
|
||||
# --- Configuration ---
|
||||
# Environment variables are loaded from the .env file.
|
||||
#
|
||||
# To get started:
|
||||
# 1. Copy this file to your project root:
|
||||
# cp config/docker-compose.example.yml docker-compose.yml
|
||||
#
|
||||
# 2. Copy the unified transcription config (RECOMMENDED):
|
||||
# cp config/env.transcription.example .env
|
||||
#
|
||||
# This supports all providers with auto-detection:
|
||||
# - OpenAI GPT-4o with diarization (set TRANSCRIPTION_MODEL=gpt-4o-transcribe-diarize)
|
||||
# - Self-hosted ASR/WhisperX (set ASR_BASE_URL=http://your-asr:9000)
|
||||
# - Legacy Whisper (set TRANSCRIPTION_MODEL=whisper-1)
|
||||
#
|
||||
# Legacy config files (still supported):
|
||||
# - config/env.whisper.example - Standard Whisper API
|
||||
# - config/env.whisperx.example - WhisperX with voice profiles
|
||||
# - config/env.asr.example - Basic ASR with diarization
|
||||
#
|
||||
# 3. Edit the .env file to add your API keys:
|
||||
# - TRANSCRIPTION_API_KEY (for OpenAI) or ASR_BASE_URL (for self-hosted)
|
||||
# - TEXT_MODEL_API_KEY (REQUIRED for summaries, titles, and chat)
|
||||
#
|
||||
# 4. Start DictIA:
|
||||
# docker compose up -d
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
environment:
|
||||
# Set log level for troubleshooting
|
||||
# Use ERROR for production (minimal logs)
|
||||
# Use INFO for debugging issues (recommended when troubleshooting)
|
||||
# Use DEBUG for detailed development logging
|
||||
- LOG_LEVEL=ERROR
|
||||
|
||||
# --- Volume Configuration ---
|
||||
# Choose ONE of the following volume configurations.
|
||||
# Option 1 (Recommended): Bind mounts to local folders.
|
||||
volumes:
|
||||
- ./uploads:/data/uploads
|
||||
- ./instance:/data/instance
|
||||
# Optional: Uncomment if using auto-export feature (ENABLE_AUTO_EXPORT=true)
|
||||
# - ./exports:/data/exports
|
||||
# Optional: Uncomment if using auto-processing feature (ENABLE_AUTO_PROCESSING=true)
|
||||
# - ./auto-process:/data/auto-process
|
||||
|
||||
# Option 2: Docker-managed volumes.
|
||||
# volumes:
|
||||
# - dictia-uploads:/data/uploads
|
||||
# - dictia-instance:/data/instance
|
||||
# # Optional: Uncomment if using auto-export feature
|
||||
# # - dictia-exports:/data/exports
|
||||
# # Optional: Uncomment if using auto-processing feature
|
||||
# # - dictia-auto-process:/data/auto-process
|
||||
259
config/env.asr.example
Normal file
259
config/env.asr.example
Normal file
@@ -0,0 +1,259 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# DictIA Configuration: ASR Endpoint (Legacy)
|
||||
#
|
||||
# ⚠️ DEPRECATION NOTICE: This configuration style is still supported but
|
||||
# we recommend using the new unified configuration in env.transcription.example
|
||||
# which supports all transcription providers with auto-detection.
|
||||
#
|
||||
# Migration: Simply set ASR_BASE_URL and the connector will auto-detect ASR mode.
|
||||
# USE_ASR_ENDPOINT=true is no longer required (but still works for backwards compat).
|
||||
#
|
||||
# Instructions:
|
||||
# 1. Copy this file to a new file named .env
|
||||
# cp env.asr.example .env
|
||||
# 2. Fill in the required URLs, API keys, and settings below.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# --- Text Generation Model (for summaries, titles, etc.) ---
|
||||
TEXT_MODEL_BASE_URL=https://openrouter.ai/api/v1
|
||||
TEXT_MODEL_API_KEY=your_openrouter_api_key
|
||||
TEXT_MODEL_NAME=openai/gpt-4o-mini
|
||||
|
||||
# --- GPT-5 Specific Settings (only used with OpenAI API and GPT-5 models) ---
|
||||
# If using GPT-5 models (gpt-5, gpt-5-mini, gpt-5-nano, gpt-5-chat-latest) with OpenAI API,
|
||||
# these parameters will be used instead of temperature.
|
||||
#
|
||||
# Example GPT-5 configuration:
|
||||
# TEXT_MODEL_BASE_URL=https://api.openai.com/v1
|
||||
# TEXT_MODEL_NAME=gpt-5-mini
|
||||
#
|
||||
# Reasoning effort: minimal, low, medium, high (default: medium)
|
||||
# - minimal: Fastest responses, minimal reasoning tokens
|
||||
# - low: Fast responses with basic reasoning
|
||||
# - medium: Balanced reasoning and speed (recommended)
|
||||
# - high: Maximum reasoning for complex tasks
|
||||
GPT5_REASONING_EFFORT=medium
|
||||
#
|
||||
# Verbosity: low, medium, high (default: medium)
|
||||
# - low: Concise responses
|
||||
# - medium: Balanced detail
|
||||
# - high: Detailed explanations
|
||||
GPT5_VERBOSITY=medium
|
||||
|
||||
# --- Chat Model Configuration (Optional) ---
|
||||
# Configure a separate model for real-time chat interactions.
|
||||
# If not set, chat will use the TEXT_MODEL_* settings above.
|
||||
#
|
||||
# Use cases:
|
||||
# - Use a faster model for chat while using a more capable model for summarization
|
||||
# - Use a cheaper model for interactive chat to reduce costs
|
||||
# - Use different service tiers for different operations
|
||||
#
|
||||
# CHAT_MODEL_API_KEY=your_chat_api_key
|
||||
# CHAT_MODEL_BASE_URL=https://openrouter.ai/api/v1
|
||||
# CHAT_MODEL_NAME=openai/gpt-4o
|
||||
|
||||
# --- Chat GPT-5 Settings (only used with OpenAI API and GPT-5 chat models) ---
|
||||
# These settings allow independent control of GPT-5 parameters for chat.
|
||||
# If not set, falls back to the main GPT5_* settings above.
|
||||
#
|
||||
# CHAT_GPT5_REASONING_EFFORT=medium
|
||||
# CHAT_GPT5_VERBOSITY=medium
|
||||
|
||||
# --- LLM Streaming Compatibility ---
|
||||
# Some LLM servers (e.g., certain vLLM configurations) don't support OpenAI's
|
||||
# stream_options parameter. If chat streaming hangs or fails, try disabling this.
|
||||
# Note: When disabled, token usage tracking for chat will not be available.
|
||||
# ENABLE_STREAM_OPTIONS=false
|
||||
|
||||
# --- Transcription Service (ASR Endpoint) ---
|
||||
# New connector architecture auto-detects ASR mode when ASR_BASE_URL is set.
|
||||
# USE_ASR_ENDPOINT=true is deprecated but still works for backwards compatibility.
|
||||
#
|
||||
# Note: ASR endpoints handle chunking internally - CHUNK_LIMIT settings are ignored.
|
||||
|
||||
# ASR Endpoint URL (setting this auto-enables ASR mode)
|
||||
# For containers in same docker-compose: Use container name and internal port
|
||||
# Example: http://whisper-asr:9000 (NOT the host port 6002 or external IP)
|
||||
# For external ASR: Use http://192.168.1.100:9000 or http://asr.example.com:9000
|
||||
ASR_BASE_URL=http://whisper-asr:9000
|
||||
|
||||
# Deprecated: No longer needed, kept for backwards compatibility
|
||||
# USE_ASR_ENDPOINT=true
|
||||
|
||||
# Speaker diarization options
|
||||
ASR_DIARIZE=true
|
||||
# ASR_MIN_SPEAKERS=1 # Hint for minimum speakers
|
||||
# ASR_MAX_SPEAKERS=5 # Hint for maximum speakers
|
||||
# ASR_RETURN_SPEAKER_EMBEDDINGS=false # Only enable for WhisperX ASR service
|
||||
|
||||
# --- ASR Chunking (for GPUs with limited memory) ---
|
||||
# Self-hosted ASR services may crash on long files due to GPU memory exhaustion.
|
||||
# Enable app-level chunking to split long files before sending to ASR.
|
||||
# Default: false (ASR service handles files internally)
|
||||
# ASR_ENABLE_CHUNKING=true
|
||||
|
||||
# Maximum audio duration per chunk in seconds (default: 7200 = 2 hours)
|
||||
# Lower this value if your GPU runs out of memory on long files.
|
||||
# Common values: 600 (10 min), 1200 (20 min), 1800 (30 min), 3600 (1 hour)
|
||||
# ASR_MAX_DURATION_SECONDS=7200
|
||||
|
||||
# --- Application Settings ---
|
||||
# Set to "true" to allow user registration, "false" to disable
|
||||
ALLOW_REGISTRATION=false
|
||||
# Comma-separated list of allowed email domains for registration.
|
||||
# Leave empty to allow all domains. Example: company.com,subsidiary.org
|
||||
REGISTRATION_ALLOWED_DOMAINS=
|
||||
SUMMARY_MAX_TOKENS=8000
|
||||
CHAT_MAX_TOKENS=5000
|
||||
|
||||
# Timezone for displaying dates and times in the UI
|
||||
# Use a valid TZ database name (e.g., "America/New_York", "Europe/London", "UTC")
|
||||
TIMEZONE="UTC"
|
||||
|
||||
# Set the logging level for the application.
|
||||
# Options: DEBUG, INFO, WARNING, ERROR
|
||||
LOG_LEVEL="INFO"
|
||||
|
||||
# --- Audio Compression ---
|
||||
# Automatically compress lossless uploads (WAV, AIFF) to save storage
|
||||
AUDIO_COMPRESS_UPLOADS=true
|
||||
|
||||
# Target codec: mp3 (lossy, smallest), flac (lossless), opus (lossy, efficient)
|
||||
AUDIO_CODEC=mp3
|
||||
|
||||
# Bitrate for lossy codecs (ignored for FLAC)
|
||||
AUDIO_BITRATE=128k
|
||||
|
||||
# Unsupported codecs - comma-separated list of codecs to exclude from supported list
|
||||
# Use this if your transcription service doesn't support certain codecs
|
||||
# Supported codecs by default: pcm_s16le, pcm_s24le, pcm_f32le, mp3, flac, opus, vorbis, aac
|
||||
# Example: AUDIO_UNSUPPORTED_CODECS=opus,vorbis
|
||||
# AUDIO_UNSUPPORTED_CODECS=
|
||||
|
||||
# --- Admin User (created on first run) ---
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_EMAIL=admin@example.com
|
||||
ADMIN_PASSWORD=changeme
|
||||
|
||||
# --- Inquire Mode (AI search across all recordings) ---
|
||||
# Set to "true" to enable semantic search and chat across all recordings
|
||||
# Requires additional dependencies (already included in Docker image)
|
||||
ENABLE_INQUIRE_MODE=false
|
||||
|
||||
# --- Automated File Processing (Black Hole Directory) ---
|
||||
# Set to "true" to enable automated file processing
|
||||
ENABLE_AUTO_PROCESSING=false
|
||||
|
||||
# --- Automated Export Settings ---
|
||||
# Automatically export transcriptions and summaries to markdown files
|
||||
ENABLE_AUTO_EXPORT=false
|
||||
|
||||
# Directory where exports will be saved (per-user subdirectories created automatically)
|
||||
AUTO_EXPORT_DIR=/data/exports
|
||||
|
||||
# What to include in exports
|
||||
AUTO_EXPORT_TRANSCRIPTION=true
|
||||
AUTO_EXPORT_SUMMARY=true
|
||||
|
||||
# Processing mode: admin_only, user_directories, or single_user
|
||||
AUTO_PROCESS_MODE=admin_only
|
||||
|
||||
# Directory to watch for new audio files
|
||||
AUTO_PROCESS_WATCH_DIR=/data/auto-process
|
||||
|
||||
# How often to check for new files (seconds)
|
||||
AUTO_PROCESS_CHECK_INTERVAL=30
|
||||
|
||||
# How long to wait (seconds) to confirm a file has stopped changing before processing.
|
||||
# Increase for slow network transfers (NFS, SMB). Default: 5
|
||||
# AUTO_PROCESS_STABILITY_TIME=5
|
||||
|
||||
# Default username for single_user mode (only used if AUTO_PROCESS_MODE=single_user)
|
||||
# AUTO_PROCESS_DEFAULT_USERNAME=admin
|
||||
|
||||
# --- Auto-Deletion & Retention Settings ---
|
||||
# Enable automated deletion of old recordings
|
||||
ENABLE_AUTO_DELETION=false
|
||||
|
||||
# Number of days to retain recordings (0 = disabled)
|
||||
# Example: 90 means recordings older than 90 days will be processed
|
||||
GLOBAL_RETENTION_DAYS=90
|
||||
|
||||
# Deletion mode: 'audio_only' keeps transcription, 'full_recording' deletes everything
|
||||
# audio_only: Deletes audio file but keeps transcription/summary/notes (recommended)
|
||||
# full_recording: Permanently deletes the entire recording from database
|
||||
DELETION_MODE=audio_only
|
||||
|
||||
# --- Permission-Based Deletion Controls ---
|
||||
# Allow all users to delete their recordings, or restrict to admins only
|
||||
# true: All users can delete their own recordings (default)
|
||||
# false: Only admins can delete recordings
|
||||
USERS_CAN_DELETE=true
|
||||
|
||||
# Delete speaker profiles when all their recordings are removed.
|
||||
# Default: false (speaker profiles and voice embeddings are preserved)
|
||||
# Set to true for privacy-sensitive deployments where biometric voice data
|
||||
# should not outlive the recordings it was derived from.
|
||||
# DELETE_ORPHANED_SPEAKERS=false
|
||||
|
||||
# --- Internal Sharing Settings ---
|
||||
# Enable user-to-user sharing of recordings (works independently of groups)
|
||||
ENABLE_INTERNAL_SHARING=false
|
||||
|
||||
# Show usernames in the UI (when sharing/viewing shared recordings)
|
||||
# true: Display usernames throughout the interface
|
||||
# false: Hide usernames (users must know each other's usernames to share)
|
||||
SHOW_USERNAMES_IN_UI=false
|
||||
|
||||
# --- Public Sharing Settings ---
|
||||
# Enable creation of public share links (anonymous access)
|
||||
# true: Users can create public links to share recordings externally (default)
|
||||
# false: Public sharing is disabled globally
|
||||
ENABLE_PUBLIC_SHARING=true
|
||||
|
||||
# Note: Admins can control public sharing permissions per-user in the admin dashboard
|
||||
# even when ENABLE_PUBLIC_SHARING is true
|
||||
|
||||
# --- Incognito Mode (HIPAA-friendly) ---
|
||||
# Enable incognito mode for privacy-sensitive transcriptions
|
||||
# When enabled, users can upload recordings that are:
|
||||
# - Processed on the server but NOT saved to the database
|
||||
# - Stored only in the browser's sessionStorage (lost when tab closes)
|
||||
# - Audio files are immediately deleted after processing
|
||||
# Useful for HIPAA compliance or sensitive recordings
|
||||
# Default: false (feature hidden)
|
||||
ENABLE_INCOGNITO_MODE=false
|
||||
|
||||
# Make incognito mode the default for in-app recordings (toggle starts ON)
|
||||
INCOGNITO_MODE_DEFAULT=false
|
||||
|
||||
# --- Video Retention ---
|
||||
# When enabled, uploaded video files keep their video stream for in-browser playback
|
||||
# The audio is extracted to a temp file for transcription, then cleaned up
|
||||
# Default: false (video uploads extract audio only, video stream is discarded)
|
||||
VIDEO_RETENTION=false
|
||||
|
||||
# --- Concurrent Uploads ---
|
||||
# Maximum number of simultaneous file uploads (default: 3)
|
||||
MAX_CONCURRENT_UPLOADS=3
|
||||
|
||||
# --- Background Processing Queues ---
|
||||
# Separate queues for transcription (slow) and summary (fast) jobs
|
||||
# This prevents slow ASR jobs from blocking quick summary generation
|
||||
|
||||
# Transcription queue workers (for ASR processing, default: 2)
|
||||
JOB_QUEUE_WORKERS=2
|
||||
|
||||
# Summary queue workers (for LLM summarization, default: 2)
|
||||
SUMMARY_QUEUE_WORKERS=2
|
||||
|
||||
# Maximum retry attempts for failed jobs (default: 3)
|
||||
JOB_MAX_RETRIES=3
|
||||
|
||||
# --- Docker Settings (rarely need to be changed) ---
|
||||
# Database URI - SQLite (default) or PostgreSQL
|
||||
SQLALCHEMY_DATABASE_URI=sqlite:////data/instance/transcriptions.db
|
||||
# For PostgreSQL, use: postgresql://username:password@hostname:5432/database_name
|
||||
# Example: postgresql://speakr:password@postgres:5432/speakr
|
||||
UPLOAD_FOLDER=/data/uploads
|
||||
109
config/env.email.example
Normal file
109
config/env.email.example
Normal file
@@ -0,0 +1,109 @@
|
||||
###############################################################################
|
||||
# Email Verification & Password Reset Configuration
|
||||
###############################################################################
|
||||
|
||||
# Enable email verification for new user registrations.
|
||||
# When enabled, new users must verify their email before full access.
|
||||
# Default: false
|
||||
ENABLE_EMAIL_VERIFICATION=false
|
||||
|
||||
# Require email verification to log in.
|
||||
# Only effective when ENABLE_EMAIL_VERIFICATION=true.
|
||||
# When true, users cannot log in until they verify their email.
|
||||
# Default: false
|
||||
REQUIRE_EMAIL_VERIFICATION=false
|
||||
|
||||
###############################################################################
|
||||
# SMTP Configuration
|
||||
###############################################################################
|
||||
|
||||
# SMTP server hostname (required for email functionality)
|
||||
# Examples: smtp.gmail.com, smtp.sendgrid.net, smtp.mailgun.org
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
|
||||
# SMTP server port
|
||||
# Common ports: 587 (TLS/STARTTLS), 465 (SSL), 25 (unencrypted)
|
||||
# Default: 587
|
||||
SMTP_PORT=587
|
||||
|
||||
# SMTP authentication username (usually your email address)
|
||||
SMTP_USERNAME=your-email@gmail.com
|
||||
|
||||
# SMTP authentication password
|
||||
# For Gmail: Use an App Password (not your regular password)
|
||||
# https://support.google.com/accounts/answer/185833
|
||||
SMTP_PASSWORD=your-app-password
|
||||
|
||||
# Use TLS/STARTTLS encryption (recommended for port 587)
|
||||
# Default: true
|
||||
SMTP_USE_TLS=true
|
||||
|
||||
# Use SSL encryption (for port 465)
|
||||
# Note: Only enable one of SMTP_USE_TLS or SMTP_USE_SSL
|
||||
# Default: false
|
||||
SMTP_USE_SSL=false
|
||||
|
||||
# Email address that appears in the "From" field
|
||||
# Should be a valid email address, ideally matching your domain
|
||||
SMTP_FROM_ADDRESS=noreply@yourdomain.com
|
||||
|
||||
# Display name that appears alongside the from address
|
||||
# Default: Speakr
|
||||
SMTP_FROM_NAME=Speakr
|
||||
|
||||
###############################################################################
|
||||
# Provider-Specific Examples
|
||||
###############################################################################
|
||||
|
||||
# --- Gmail ---
|
||||
# SMTP_HOST=smtp.gmail.com
|
||||
# SMTP_PORT=587
|
||||
# SMTP_USE_TLS=true
|
||||
# SMTP_USERNAME=your-email@gmail.com
|
||||
# SMTP_PASSWORD=your-app-password # Generate at https://myaccount.google.com/apppasswords
|
||||
|
||||
# --- SendGrid ---
|
||||
# SMTP_HOST=smtp.sendgrid.net
|
||||
# SMTP_PORT=587
|
||||
# SMTP_USE_TLS=true
|
||||
# SMTP_USERNAME=apikey
|
||||
# SMTP_PASSWORD=your-sendgrid-api-key
|
||||
|
||||
# --- Mailgun ---
|
||||
# SMTP_HOST=smtp.mailgun.org
|
||||
# SMTP_PORT=587
|
||||
# SMTP_USE_TLS=true
|
||||
# SMTP_USERNAME=postmaster@your-domain.mailgun.org
|
||||
# SMTP_PASSWORD=your-mailgun-password
|
||||
|
||||
# --- Amazon SES ---
|
||||
# SMTP_HOST=email-smtp.us-east-1.amazonaws.com
|
||||
# SMTP_PORT=587
|
||||
# SMTP_USE_TLS=true
|
||||
# SMTP_USERNAME=your-ses-smtp-username
|
||||
# SMTP_PASSWORD=your-ses-smtp-password
|
||||
|
||||
# --- Microsoft 365 / Outlook ---
|
||||
# SMTP_HOST=smtp.office365.com
|
||||
# SMTP_PORT=587
|
||||
# SMTP_USE_TLS=true
|
||||
# SMTP_USERNAME=your-email@yourdomain.com
|
||||
# SMTP_PASSWORD=your-password
|
||||
|
||||
###############################################################################
|
||||
# Notes
|
||||
###############################################################################
|
||||
|
||||
# Token Expiry Times:
|
||||
# - Email verification links expire after 24 hours
|
||||
# - Password reset links expire after 1 hour
|
||||
|
||||
# Migration Behavior:
|
||||
# - Existing users are automatically marked as email_verified=true
|
||||
# - New users (when feature is enabled) start as email_verified=false
|
||||
|
||||
# Security Recommendations:
|
||||
# - Always use TLS or SSL encryption
|
||||
# - Use app-specific passwords when available (Gmail, etc.)
|
||||
# - Consider using a dedicated email service (SendGrid, Mailgun, SES)
|
||||
# - Set a strong SECRET_KEY in your Flask configuration
|
||||
32
config/env.sso.example
Normal file
32
config/env.sso.example
Normal file
@@ -0,0 +1,32 @@
|
||||
###############################################################################
|
||||
# SSO (OIDC) Authentication
|
||||
###############################################################################
|
||||
|
||||
# Enable SSO (Single Sign-On) authentication. Requires discovery URL and client credentials.
|
||||
ENABLE_SSO=false
|
||||
|
||||
# Display name for the provider (shown in UI button)
|
||||
SSO_PROVIDER_NAME=Keycloak
|
||||
|
||||
# OIDC client credentials
|
||||
SSO_CLIENT_ID=speakr
|
||||
SSO_CLIENT_SECRET=change-me
|
||||
|
||||
# OIDC discovery document URL (well-known endpoint)
|
||||
SSO_DISCOVERY_URL=https://keycloak.example.com/realms/master/.well-known/openid-configuration
|
||||
|
||||
# Public redirect URI exposed by Speakr (must be registered in the IdP)
|
||||
SSO_REDIRECT_URI=https://speakr.example.com/auth/sso/callback
|
||||
|
||||
# Auto-registration settings
|
||||
# Allow automatic account creation for new users signing in via SSO.
|
||||
SSO_AUTO_REGISTER=true
|
||||
|
||||
# Comma-separated list of allowed email domains for auto-registration.
|
||||
# Leave empty to allow all domains (e.g., example.com,company.org).
|
||||
SSO_ALLOWED_DOMAINS=
|
||||
|
||||
# Claims used to map user profile fields
|
||||
SSO_DEFAULT_USERNAME_CLAIM=preferred_username
|
||||
SSO_DEFAULT_NAME_CLAIM=name
|
||||
|
||||
289
config/env.transcription.example
Normal file
289
config/env.transcription.example
Normal file
@@ -0,0 +1,289 @@
|
||||
# =============================================================================
|
||||
# Transcription Connector Configuration
|
||||
# =============================================================================
|
||||
#
|
||||
# DictIA supports multiple transcription providers through a connector-based
|
||||
# architecture. This file documents all available configuration options.
|
||||
#
|
||||
# Quick Start (Simplified):
|
||||
# 1. For OpenAI with diarization: Set TRANSCRIPTION_MODEL=gpt-4o-transcribe-diarize
|
||||
# 2. For self-hosted ASR: Set ASR_BASE_URL=http://your-asr:9000
|
||||
# 3. For legacy Whisper: Set TRANSCRIPTION_API_KEY and optionally TRANSCRIPTION_MODEL
|
||||
#
|
||||
# Auto-Detection Priority:
|
||||
# 1. TRANSCRIPTION_CONNECTOR - explicit connector name (if you need full control)
|
||||
# 2. ASR_BASE_URL - if set, uses ASR endpoint connector
|
||||
# 3. TRANSCRIPTION_MODEL contains 'gpt-4o' - uses OpenAI Transcribe connector
|
||||
# 4. Default - uses OpenAI Whisper connector with TRANSCRIPTION_MODEL or whisper-1
|
||||
|
||||
# =============================================================================
|
||||
# TEXT GENERATION MODEL (REQUIRED for summaries, titles, chat)
|
||||
# =============================================================================
|
||||
# DictIA uses a text/LLM model for generating summaries, titles, and chat.
|
||||
# This is separate from the transcription model (STT).
|
||||
#
|
||||
# You can use OpenRouter (recommended - access to many models) or direct OpenAI API.
|
||||
|
||||
# OpenRouter example (recommended - supports many models):
|
||||
TEXT_MODEL_BASE_URL=https://openrouter.ai/api/v1
|
||||
TEXT_MODEL_API_KEY=your_openrouter_api_key
|
||||
TEXT_MODEL_NAME=openai/gpt-4o-mini
|
||||
|
||||
# OpenAI direct example:
|
||||
# TEXT_MODEL_BASE_URL=https://api.openai.com/v1
|
||||
# TEXT_MODEL_API_KEY=sk-your_openai_api_key
|
||||
# TEXT_MODEL_NAME=gpt-4o-mini
|
||||
|
||||
# --- GPT-5 Specific Settings (only used with OpenAI API and GPT-5 models) ---
|
||||
# Reasoning effort: minimal, low, medium, high (default: medium)
|
||||
GPT5_REASONING_EFFORT=medium
|
||||
# Verbosity: low, medium, high (default: medium)
|
||||
GPT5_VERBOSITY=medium
|
||||
|
||||
# --- Chat Model Configuration (Optional) ---
|
||||
# Configure a separate model for real-time chat interactions.
|
||||
# If not set, chat will use the TEXT_MODEL_* settings above.
|
||||
# CHAT_MODEL_API_KEY=your_chat_api_key
|
||||
# CHAT_MODEL_BASE_URL=https://openrouter.ai/api/v1
|
||||
# CHAT_MODEL_NAME=openai/gpt-4o
|
||||
|
||||
# =============================================================================
|
||||
# CONNECTOR SELECTION (Auto-detected if not set)
|
||||
# =============================================================================
|
||||
# Options: openai_whisper, openai_transcribe, asr_endpoint
|
||||
# Leave empty to auto-detect based on other settings
|
||||
# TRANSCRIPTION_CONNECTOR=
|
||||
|
||||
# Feature flag to enable/disable new connector architecture (default: true)
|
||||
# Set to false to use legacy code path for troubleshooting
|
||||
# USE_NEW_TRANSCRIPTION_ARCHITECTURE=true
|
||||
|
||||
# =============================================================================
|
||||
# OPENAI CONFIGURATION (Required for openai_whisper and openai_transcribe)
|
||||
# =============================================================================
|
||||
TRANSCRIPTION_API_KEY=your_openai_api_key
|
||||
TRANSCRIPTION_BASE_URL=https://api.openai.com/v1
|
||||
|
||||
# Model Selection - determines which connector is used:
|
||||
#
|
||||
# whisper-1 - Legacy Whisper model, no diarization, $0.006/min
|
||||
# Supports: srt, vtt, json, verbose_json output formats
|
||||
#
|
||||
# gpt-4o-transcribe - High quality transcription, no diarization, $0.006/min
|
||||
# Better accuracy than whisper-1, accepts prompts
|
||||
#
|
||||
# gpt-4o-mini-transcribe - Cost-effective option, no diarization, $0.003/min
|
||||
# Good for high-volume, budget-conscious use
|
||||
#
|
||||
# gpt-4o-transcribe-diarize - Speaker diarization!, $0.006/min
|
||||
# Identifies speakers as A, B, C, D...
|
||||
# Requires chunking_strategy for audio >30s
|
||||
#
|
||||
TRANSCRIPTION_MODEL=gpt-4o-transcribe-diarize
|
||||
|
||||
# Legacy Whisper model name (used when TRANSCRIPTION_MODEL is not set)
|
||||
# WHISPER_MODEL=whisper-1
|
||||
|
||||
# =============================================================================
|
||||
# ASR ENDPOINT CONFIGURATION (For self-hosted whisper services)
|
||||
# =============================================================================
|
||||
# Note: USE_ASR_ENDPOINT is deprecated. Just set ASR_BASE_URL instead.
|
||||
# The connector will auto-detect ASR mode when ASR_BASE_URL is set.
|
||||
# USE_ASR_ENDPOINT=true # Deprecated - kept for backwards compatibility
|
||||
|
||||
# Base URL of your ASR service (required if USE_ASR_ENDPOINT=true)
|
||||
# Supports: whisper-asr-webservice, WhisperX, and compatible services
|
||||
# ASR_BASE_URL=http://whisper-asr:9000
|
||||
|
||||
# Request timeout in seconds (default: 1800 = 30 minutes)
|
||||
# Increase for very long audio files
|
||||
# ASR_TIMEOUT=1800
|
||||
|
||||
# Enable speaker diarization (default: true)
|
||||
# ASR_DIARIZE=true
|
||||
|
||||
# Speaker count hints (optional, helps with diarization accuracy)
|
||||
# ASR_MIN_SPEAKERS=1
|
||||
# ASR_MAX_SPEAKERS=5
|
||||
|
||||
# Return speaker embeddings for speaker identification (WhisperX only)
|
||||
# Enables automatic speaker matching across recordings
|
||||
# ASR_RETURN_SPEAKER_EMBEDDINGS=false
|
||||
|
||||
# =============================================================================
|
||||
# CHUNKING CONFIGURATION (For large files)
|
||||
# =============================================================================
|
||||
# Chunking is now connector-aware with this priority:
|
||||
# 1. Connector handles internally (openai_transcribe, asr_endpoint) → No app chunking
|
||||
# 2. ENABLE_CHUNKING=false → Disable chunking (only affects openai_whisper)
|
||||
# 3. CHUNK_LIMIT set → Use your settings
|
||||
# 4. Connector defaults → Use connector's recommended limits
|
||||
# 5. App default → 20MB size-based
|
||||
#
|
||||
# For openai_transcribe/asr_endpoint: These settings are IGNORED (connector handles it)
|
||||
# For openai_whisper: These settings control chunking behavior
|
||||
|
||||
# ENABLE_CHUNKING=false # Uncomment to disable chunking for openai_whisper
|
||||
|
||||
# Chunk limit - supports size (20MB) or duration (600s, 10m)
|
||||
CHUNK_LIMIT=20MB
|
||||
|
||||
# Overlap between chunks in seconds (helps with transcription accuracy at boundaries)
|
||||
CHUNK_OVERLAP_SECONDS=3
|
||||
|
||||
# =============================================================================
|
||||
# EXAMPLE CONFIGURATIONS (Simplified)
|
||||
# =============================================================================
|
||||
#
|
||||
# --- OpenAI with Speaker Diarization (Recommended) ---
|
||||
# Just two environment variables needed:
|
||||
# TRANSCRIPTION_API_KEY=sk-xxx
|
||||
# TRANSCRIPTION_MODEL=gpt-4o-transcribe-diarize
|
||||
#
|
||||
# --- Self-hosted WhisperX (Best for privacy) ---
|
||||
# Just one environment variable needed (auto-detects ASR mode):
|
||||
# ASR_BASE_URL=http://whisper-asr:9000
|
||||
# Optional:
|
||||
# ASR_DIARIZE=true
|
||||
# ASR_RETURN_SPEAKER_EMBEDDINGS=true
|
||||
#
|
||||
# --- OpenAI Whisper (Legacy, no diarization) ---
|
||||
# TRANSCRIPTION_API_KEY=sk-xxx
|
||||
# TRANSCRIPTION_MODEL=whisper-1
|
||||
#
|
||||
# --- Custom Whisper model (local or compatible endpoint) ---
|
||||
# TRANSCRIPTION_API_KEY=not-needed
|
||||
# TRANSCRIPTION_BASE_URL=http://localhost:8080/v1
|
||||
# TRANSCRIPTION_MODEL=Systran/faster-distil-whisper-large-v3
|
||||
|
||||
# =============================================================================
|
||||
# APPLICATION SETTINGS
|
||||
# =============================================================================
|
||||
|
||||
# --- Admin User (created on first run) ---
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_EMAIL=admin@example.com
|
||||
ADMIN_PASSWORD=changeme
|
||||
|
||||
# --- Registration & Access ---
|
||||
ALLOW_REGISTRATION=false
|
||||
# Comma-separated list of allowed email domains for registration.
|
||||
# Leave empty to allow all domains. Example: company.com,subsidiary.org
|
||||
REGISTRATION_ALLOWED_DOMAINS=
|
||||
|
||||
# --- Token Limits ---
|
||||
SUMMARY_MAX_TOKENS=8000
|
||||
CHAT_MAX_TOKENS=5000
|
||||
|
||||
# --- Timezone ---
|
||||
# Use a valid TZ database name (e.g., "America/New_York", "Europe/London", "UTC")
|
||||
TIMEZONE="UTC"
|
||||
|
||||
# --- Logging ---
|
||||
LOG_LEVEL="INFO"
|
||||
|
||||
# =============================================================================
|
||||
# AUDIO PROCESSING
|
||||
# =============================================================================
|
||||
|
||||
# --- Audio Compression ---
|
||||
# Automatically compress lossless uploads (WAV, AIFF) to save storage
|
||||
AUDIO_COMPRESS_UPLOADS=true
|
||||
|
||||
# Target codec: mp3 (lossy, smallest), flac (lossless), opus (lossy, efficient)
|
||||
AUDIO_CODEC=mp3
|
||||
|
||||
# Bitrate for lossy codecs (ignored for FLAC)
|
||||
AUDIO_BITRATE=128k
|
||||
|
||||
# Unsupported codecs - comma-separated list of codecs to exclude
|
||||
# Example: AUDIO_UNSUPPORTED_CODECS=opus,vorbis
|
||||
# AUDIO_UNSUPPORTED_CODECS=
|
||||
|
||||
# =============================================================================
|
||||
# OPTIONAL FEATURES
|
||||
# =============================================================================
|
||||
|
||||
# --- Inquire Mode (AI search across all recordings) ---
|
||||
ENABLE_INQUIRE_MODE=false
|
||||
|
||||
# --- Automated File Processing (Black Hole Directory) ---
|
||||
ENABLE_AUTO_PROCESSING=false
|
||||
# AUTO_PROCESS_MODE=admin_only
|
||||
# AUTO_PROCESS_WATCH_DIR=/data/auto-process
|
||||
|
||||
# --- Automated Export ---
|
||||
ENABLE_AUTO_EXPORT=false
|
||||
# AUTO_EXPORT_DIR=/data/exports
|
||||
|
||||
# --- Auto-Deletion & Retention ---
|
||||
ENABLE_AUTO_DELETION=false
|
||||
# GLOBAL_RETENTION_DAYS=90
|
||||
# DELETION_MODE=audio_only
|
||||
|
||||
# --- Sharing Settings ---
|
||||
ENABLE_INTERNAL_SHARING=false
|
||||
ENABLE_PUBLIC_SHARING=true
|
||||
# SHOW_USERNAMES_IN_UI=false
|
||||
|
||||
# --- Permission Controls ---
|
||||
USERS_CAN_DELETE=true
|
||||
|
||||
# Delete speaker profiles when all their recordings are removed.
|
||||
# Default: false (speaker profiles and voice embeddings are preserved)
|
||||
# Set to true for privacy-sensitive deployments where biometric voice data
|
||||
# should not outlive the recordings it was derived from.
|
||||
# DELETE_ORPHANED_SPEAKERS=false
|
||||
|
||||
# --- Video Retention ---
|
||||
# When enabled, uploaded video files keep their video stream for in-browser playback
|
||||
# The audio is extracted to a temp file for transcription, then cleaned up
|
||||
# Default: false (video uploads extract audio only, video stream is discarded)
|
||||
VIDEO_RETENTION=false
|
||||
|
||||
# --- Video Passthrough to ASR ---
|
||||
# Send original video files directly to ASR without extracting audio.
|
||||
# Useful for custom ASR backends that handle video internally (e.g., multi-track audio extraction).
|
||||
# When enabled, video files bypass audio extraction, codec conversion, and chunking.
|
||||
# Only affects video files — audio uploads are processed normally.
|
||||
# Default: false
|
||||
# VIDEO_PASSTHROUGH_ASR=false
|
||||
|
||||
# --- Concurrent Uploads ---
|
||||
# Maximum number of simultaneous file uploads (default: 3)
|
||||
MAX_CONCURRENT_UPLOADS=3
|
||||
|
||||
# =============================================================================
|
||||
# BACKGROUND PROCESSING
|
||||
# =============================================================================
|
||||
|
||||
# Transcription queue workers (default: 2)
|
||||
JOB_QUEUE_WORKERS=2
|
||||
|
||||
# Summary queue workers (default: 2)
|
||||
SUMMARY_QUEUE_WORKERS=2
|
||||
|
||||
# Maximum retry attempts for failed jobs (default: 3)
|
||||
JOB_MAX_RETRIES=3
|
||||
|
||||
# =============================================================================
|
||||
# DOCKER/DATABASE SETTINGS
|
||||
# =============================================================================
|
||||
|
||||
# Database URI - SQLite (default) or PostgreSQL
|
||||
SQLALCHEMY_DATABASE_URI=sqlite:////data/instance/transcriptions.db
|
||||
# For PostgreSQL: postgresql://username:password@hostname:5432/database_name
|
||||
|
||||
UPLOAD_FOLDER=/data/uploads
|
||||
|
||||
# =============================================================================
|
||||
# FUTURE: Additional Provider Notes
|
||||
# =============================================================================
|
||||
# The connector architecture is designed to support additional providers.
|
||||
# Future connectors may include:
|
||||
#
|
||||
# - Deepgram: Known for excellent diarization and real-time transcription
|
||||
# - AssemblyAI: Strong diarization with speaker labels
|
||||
# - Google Cloud Speech-to-Text: Enterprise-grade with speaker diarization
|
||||
#
|
||||
# To request a new connector, please open an issue on GitHub.
|
||||
256
config/env.whisper.example
Normal file
256
config/env.whisper.example
Normal file
@@ -0,0 +1,256 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# DictIA Configuration: Standard Whisper API (Legacy)
|
||||
#
|
||||
# ⚠️ DEPRECATION NOTICE: This configuration style is still supported but
|
||||
# we recommend using the new unified configuration in env.transcription.example
|
||||
# which supports all transcription providers with auto-detection.
|
||||
#
|
||||
# Migration: See TRANSCRIPTION_CONNECTOR documentation in env.transcription.example
|
||||
# For OpenAI Whisper, simply set:
|
||||
# TRANSCRIPTION_API_KEY=your_key
|
||||
# TRANSCRIPTION_MODEL=whisper-1 (or gpt-4o-transcribe-diarize for diarization)
|
||||
#
|
||||
# Instructions:
|
||||
# 1. Copy this file to a new file named .env
|
||||
# cp env.whisper.example .env
|
||||
# 2. Fill in the required API keys and settings below.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# --- Text Generation Model (for summaries, titles, etc.) ---
|
||||
TEXT_MODEL_BASE_URL=https://openrouter.ai/api/v1
|
||||
TEXT_MODEL_API_KEY=your_openrouter_api_key
|
||||
TEXT_MODEL_NAME=openai/gpt-4o-mini
|
||||
|
||||
# --- GPT-5 Specific Settings (only used with OpenAI API and GPT-5 models) ---
|
||||
# If using GPT-5 models (gpt-5, gpt-5-mini, gpt-5-nano, gpt-5-chat-latest) with OpenAI API,
|
||||
# these parameters will be used instead of temperature.
|
||||
#
|
||||
# Example GPT-5 configuration:
|
||||
# TEXT_MODEL_BASE_URL=https://api.openai.com/v1
|
||||
# TEXT_MODEL_NAME=gpt-5-mini
|
||||
#
|
||||
# Reasoning effort: minimal, low, medium, high (default: medium)
|
||||
# - minimal: Fastest responses, minimal reasoning tokens
|
||||
# - low: Fast responses with basic reasoning
|
||||
# - medium: Balanced reasoning and speed (recommended)
|
||||
# - high: Maximum reasoning for complex tasks
|
||||
GPT5_REASONING_EFFORT=medium
|
||||
#
|
||||
# Verbosity: low, medium, high (default: medium)
|
||||
# - low: Concise responses
|
||||
# - medium: Balanced detail
|
||||
# - high: Detailed explanations
|
||||
GPT5_VERBOSITY=medium
|
||||
|
||||
# --- Chat Model Configuration (Optional) ---
|
||||
# Configure a separate model for real-time chat interactions.
|
||||
# If not set, chat will use the TEXT_MODEL_* settings above.
|
||||
#
|
||||
# Use cases:
|
||||
# - Use a faster model for chat while using a more capable model for summarization
|
||||
# - Use a cheaper model for interactive chat to reduce costs
|
||||
# - Use different service tiers for different operations
|
||||
#
|
||||
# CHAT_MODEL_API_KEY=your_chat_api_key
|
||||
# CHAT_MODEL_BASE_URL=https://openrouter.ai/api/v1
|
||||
# CHAT_MODEL_NAME=openai/gpt-4o
|
||||
|
||||
# --- Chat GPT-5 Settings (only used with OpenAI API and GPT-5 chat models) ---
|
||||
# These settings allow independent control of GPT-5 parameters for chat.
|
||||
# If not set, falls back to the main GPT5_* settings above.
|
||||
#
|
||||
# CHAT_GPT5_REASONING_EFFORT=medium
|
||||
# CHAT_GPT5_VERBOSITY=medium
|
||||
|
||||
# --- LLM Streaming Compatibility ---
|
||||
# Some LLM servers (e.g., certain vLLM configurations) don't support OpenAI's
|
||||
# stream_options parameter. If chat streaming hangs or fails, try disabling this.
|
||||
# Note: When disabled, token usage tracking for chat will not be available.
|
||||
# ENABLE_STREAM_OPTIONS=false
|
||||
|
||||
# --- Transcription Service (OpenAI Whisper API) ---
|
||||
# New connector architecture is enabled by default.
|
||||
# Available models:
|
||||
# whisper-1 - Legacy, no diarization
|
||||
# gpt-4o-transcribe - High quality, no diarization
|
||||
# gpt-4o-mini-transcribe - Cost-effective, no diarization
|
||||
# gpt-4o-transcribe-diarize - Speaker diarization! (recommended)
|
||||
TRANSCRIPTION_BASE_URL=https://api.openai.com/v1
|
||||
TRANSCRIPTION_API_KEY=your_openai_api_key
|
||||
TRANSCRIPTION_MODEL=whisper-1
|
||||
|
||||
# Legacy model name (deprecated, use TRANSCRIPTION_MODEL instead)
|
||||
# WHISPER_MODEL=whisper-1
|
||||
|
||||
# --- Application Settings ---
|
||||
# Set to "true" to allow user registration, "false" to disable
|
||||
ALLOW_REGISTRATION=false
|
||||
# Comma-separated list of allowed email domains for registration.
|
||||
# Leave empty to allow all domains. Example: company.com,subsidiary.org
|
||||
REGISTRATION_ALLOWED_DOMAINS=
|
||||
SUMMARY_MAX_TOKENS=8000
|
||||
CHAT_MAX_TOKENS=5000
|
||||
|
||||
# Timezone for displaying dates and times in the UI
|
||||
# Use a valid TZ database name (e.g., "America/New_York", "Europe/London", "UTC")
|
||||
TIMEZONE="UTC"
|
||||
|
||||
# Set the logging level for the application.
|
||||
# Options: DEBUG, INFO, WARNING, ERROR
|
||||
LOG_LEVEL="INFO"
|
||||
|
||||
# --- Large File Chunking ---
|
||||
# Chunking is now connector-aware:
|
||||
# - openai_transcribe/asr_endpoint: Handled internally, these settings ignored
|
||||
# - openai_whisper: Uses these settings for files >25MB
|
||||
#
|
||||
# ENABLE_CHUNKING=false # Uncomment to disable (only for openai_whisper)
|
||||
|
||||
# Chunk limit - supports size (20MB) or duration (600s, 10m)
|
||||
CHUNK_LIMIT=20MB
|
||||
|
||||
# Overlap between chunks (seconds)
|
||||
CHUNK_OVERLAP_SECONDS=3
|
||||
|
||||
# --- Audio Compression ---
|
||||
# Automatically compress lossless uploads (WAV, AIFF) to save storage
|
||||
AUDIO_COMPRESS_UPLOADS=true
|
||||
|
||||
# Target codec: mp3 (lossy, smallest), flac (lossless), opus (lossy, efficient)
|
||||
AUDIO_CODEC=mp3
|
||||
|
||||
# Bitrate for lossy codecs (ignored for FLAC)
|
||||
AUDIO_BITRATE=128k
|
||||
|
||||
# Unsupported codecs - comma-separated list of codecs to exclude from supported list
|
||||
# Use this if your transcription service doesn't support certain codecs
|
||||
# Supported codecs by default: pcm_s16le, pcm_s24le, pcm_f32le, mp3, flac, opus, vorbis, aac
|
||||
# Example: AUDIO_UNSUPPORTED_CODECS=opus,vorbis
|
||||
# AUDIO_UNSUPPORTED_CODECS=
|
||||
|
||||
# --- Admin User (created on first run) ---
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_EMAIL=admin@example.com
|
||||
ADMIN_PASSWORD=changeme
|
||||
|
||||
# --- Inquire Mode (AI search across all recordings) ---
|
||||
# Set to "true" to enable semantic search and chat across all recordings
|
||||
# Requires additional dependencies (already included in Docker image)
|
||||
ENABLE_INQUIRE_MODE=false
|
||||
|
||||
# --- Automated File Processing (Black Hole Directory) ---
|
||||
# Set to "true" to enable automated file processing
|
||||
ENABLE_AUTO_PROCESSING=false
|
||||
|
||||
# --- Automated Export Settings ---
|
||||
# Automatically export transcriptions and summaries to markdown files
|
||||
ENABLE_AUTO_EXPORT=false
|
||||
|
||||
# Directory where exports will be saved (per-user subdirectories created automatically)
|
||||
AUTO_EXPORT_DIR=/data/exports
|
||||
|
||||
# What to include in exports
|
||||
AUTO_EXPORT_TRANSCRIPTION=true
|
||||
AUTO_EXPORT_SUMMARY=true
|
||||
|
||||
# Processing mode: admin_only, user_directories, or single_user
|
||||
AUTO_PROCESS_MODE=admin_only
|
||||
|
||||
# Directory to watch for new audio files
|
||||
AUTO_PROCESS_WATCH_DIR=/data/auto-process
|
||||
|
||||
# How often to check for new files (seconds)
|
||||
AUTO_PROCESS_CHECK_INTERVAL=30
|
||||
|
||||
# How long to wait (seconds) to confirm a file has stopped changing before processing.
|
||||
# Increase for slow network transfers (NFS, SMB). Default: 5
|
||||
# AUTO_PROCESS_STABILITY_TIME=5
|
||||
|
||||
# Default username for single_user mode (only used if AUTO_PROCESS_MODE=single_user)
|
||||
# AUTO_PROCESS_DEFAULT_USERNAME=admin
|
||||
|
||||
# --- Auto-Deletion & Retention Settings ---
|
||||
# Enable automated deletion of old recordings
|
||||
ENABLE_AUTO_DELETION=false
|
||||
|
||||
# Number of days to retain recordings (0 = disabled)
|
||||
# Example: 90 means recordings older than 90 days will be processed
|
||||
GLOBAL_RETENTION_DAYS=90
|
||||
|
||||
# Deletion mode: 'audio_only' keeps transcription, 'full_recording' deletes everything
|
||||
# audio_only: Deletes audio file but keeps transcription/summary/notes (recommended)
|
||||
# full_recording: Permanently deletes the entire recording from database
|
||||
DELETION_MODE=audio_only
|
||||
|
||||
# --- Permission-Based Deletion Controls ---
|
||||
# Allow all users to delete their recordings, or restrict to admins only
|
||||
# true: All users can delete their own recordings (default)
|
||||
# false: Only admins can delete recordings
|
||||
USERS_CAN_DELETE=true
|
||||
|
||||
# Delete speaker profiles when all their recordings are removed.
|
||||
# Default: false (speaker profiles and voice embeddings are preserved)
|
||||
# Set to true for privacy-sensitive deployments where biometric voice data
|
||||
# should not outlive the recordings it was derived from.
|
||||
# DELETE_ORPHANED_SPEAKERS=false
|
||||
|
||||
# --- Internal Sharing Settings ---
|
||||
# Enable user-to-user sharing of recordings (works independently of groups)
|
||||
ENABLE_INTERNAL_SHARING=false
|
||||
|
||||
# Show usernames in the UI (when sharing/viewing shared recordings)
|
||||
# true: Display usernames throughout the interface
|
||||
# false: Hide usernames (users must know each other's usernames to share)
|
||||
SHOW_USERNAMES_IN_UI=false
|
||||
|
||||
# --- Public Sharing Settings ---
|
||||
# Enable creation of public share links (anonymous access)
|
||||
# true: Users can create public links to share recordings externally (default)
|
||||
# false: Public sharing is disabled globally
|
||||
ENABLE_PUBLIC_SHARING=true
|
||||
|
||||
# Note: Admins can control public sharing permissions per-user in the admin dashboard
|
||||
# even when ENABLE_PUBLIC_SHARING is true
|
||||
|
||||
# --- Incognito Mode (HIPAA-friendly) ---
|
||||
# Enable incognito mode for privacy-sensitive transcriptions
|
||||
# When enabled, users can upload recordings that are:
|
||||
# - Processed on the server but NOT saved to the database
|
||||
# - Stored only in the browser's sessionStorage (lost when tab closes)
|
||||
# - Audio files are immediately deleted after processing
|
||||
# Useful for HIPAA compliance or sensitive recordings
|
||||
# Default: false (feature hidden)
|
||||
ENABLE_INCOGNITO_MODE=false
|
||||
|
||||
# Make incognito mode the default for in-app recordings (toggle starts ON)
|
||||
INCOGNITO_MODE_DEFAULT=false
|
||||
|
||||
# --- Video Retention ---
|
||||
# When enabled, uploaded video files keep their video stream for in-browser playback
|
||||
# The audio is extracted to a temp file for transcription, then cleaned up
|
||||
# Default: false (video uploads extract audio only, video stream is discarded)
|
||||
VIDEO_RETENTION=false
|
||||
|
||||
# --- Concurrent Uploads ---
|
||||
# Maximum number of simultaneous file uploads (default: 3)
|
||||
MAX_CONCURRENT_UPLOADS=3
|
||||
|
||||
# --- Background Processing Queues ---
|
||||
# Separate queues for transcription (slow) and summary (fast) jobs
|
||||
# This prevents slow ASR jobs from blocking quick summary generation
|
||||
|
||||
# Transcription queue workers (for ASR processing, default: 2)
|
||||
JOB_QUEUE_WORKERS=2
|
||||
|
||||
# Summary queue workers (for LLM summarization, default: 2)
|
||||
SUMMARY_QUEUE_WORKERS=2
|
||||
|
||||
# Maximum retry attempts for failed jobs (default: 3)
|
||||
JOB_MAX_RETRIES=3
|
||||
|
||||
# --- Docker Settings (rarely need to be changed) ---
|
||||
# Database URI - SQLite (default) or PostgreSQL
|
||||
SQLALCHEMY_DATABASE_URI=sqlite:////data/instance/transcriptions.db
|
||||
# For PostgreSQL, use: postgresql://username:password@hostname:5432/database_name
|
||||
# Example: postgresql://speakr:password@postgres:5432/speakr
|
||||
UPLOAD_FOLDER=/data/uploads
|
||||
241
config/env.whisperx.example
Normal file
241
config/env.whisperx.example
Normal file
@@ -0,0 +1,241 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# DictIA Configuration: WhisperX ASR Endpoint (with Voice Profiles)
|
||||
#
|
||||
# ⚠️ DEPRECATION NOTICE: This configuration style is still supported but
|
||||
# we recommend using the new unified configuration in env.transcription.example
|
||||
# which supports all transcription providers with auto-detection.
|
||||
#
|
||||
# Migration: Simply set ASR_BASE_URL and the connector will auto-detect ASR mode.
|
||||
# USE_ASR_ENDPOINT=true is no longer required (but still works for backwards compat).
|
||||
#
|
||||
# This configuration is for use with the WhisperX ASR Service:
|
||||
# https://github.com/murtaza-nasir/whisperx-asr-service
|
||||
#
|
||||
# Features supported:
|
||||
# - Speaker diarization with pyannote/speaker-diarization-community-1
|
||||
# - Voice profile embeddings (256-dimensional) for speaker recognition
|
||||
# - Automatic speaker matching across recordings
|
||||
# - Better timestamp alignment between speakers and words
|
||||
#
|
||||
# Instructions:
|
||||
# 1. Copy this file to a new file named .env
|
||||
# cp config/env.whisperx.example .env
|
||||
# 2. Fill in the required URLs, API keys, and settings below.
|
||||
# 3. Set up WhisperX ASR Service (see installation guide)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# --- Text Generation Model (for summaries, titles, etc.) ---
|
||||
TEXT_MODEL_BASE_URL=https://openrouter.ai/api/v1
|
||||
TEXT_MODEL_API_KEY=your_openrouter_api_key
|
||||
TEXT_MODEL_NAME=openai/gpt-4o-mini
|
||||
|
||||
# --- GPT-5 Specific Settings (only used with OpenAI API and GPT-5 models) ---
|
||||
# If using GPT-5 models (gpt-5, gpt-5-mini, gpt-5-nano, gpt-5-chat-latest) with OpenAI API,
|
||||
# these parameters will be used instead of temperature.
|
||||
#
|
||||
# Example GPT-5 configuration:
|
||||
# TEXT_MODEL_BASE_URL=https://api.openai.com/v1
|
||||
# TEXT_MODEL_NAME=gpt-5-mini
|
||||
#
|
||||
# Reasoning effort: minimal, low, medium, high (default: medium)
|
||||
# - minimal: Fastest responses, minimal reasoning tokens
|
||||
# - low: Fast responses with basic reasoning
|
||||
# - medium: Balanced reasoning and speed (recommended)
|
||||
# - high: Maximum reasoning for complex tasks
|
||||
GPT5_REASONING_EFFORT=medium
|
||||
#
|
||||
# Verbosity: low, medium, high (default: medium)
|
||||
# - low: Concise responses
|
||||
# - medium: Balanced detail
|
||||
# - high: Detailed explanations
|
||||
GPT5_VERBOSITY=medium
|
||||
|
||||
# --- Auto-Identify Speaker Response Format ---
|
||||
# When enabled, auto-identify uses JSON Schema response format (structured outputs)
|
||||
# to constrain LLM output to valid SPEAKER_XX keys. Falls back to json_object mode
|
||||
# if the model doesn't support it. Leave disabled for widest model compatibility.
|
||||
# AUTO_IDENTIFY_RESPONSE_SCHEMA=1
|
||||
|
||||
# --- Chat Model Configuration (Optional) ---
|
||||
# Configure a separate model for real-time chat interactions.
|
||||
# If not set, chat will use the TEXT_MODEL_* settings above.
|
||||
#
|
||||
# Use cases:
|
||||
# - Use a faster model for chat while using a more capable model for summarization
|
||||
# - Use a cheaper model for interactive chat to reduce costs
|
||||
# - Use different service tiers for different operations
|
||||
#
|
||||
# CHAT_MODEL_API_KEY=your_chat_api_key
|
||||
# CHAT_MODEL_BASE_URL=https://openrouter.ai/api/v1
|
||||
# CHAT_MODEL_NAME=openai/gpt-4o
|
||||
|
||||
# --- Chat GPT-5 Settings (only used with OpenAI API and GPT-5 chat models) ---
|
||||
# These settings allow independent control of GPT-5 parameters for chat.
|
||||
# If not set, falls back to the main GPT5_* settings above.
|
||||
#
|
||||
# CHAT_GPT5_REASONING_EFFORT=medium
|
||||
# CHAT_GPT5_VERBOSITY=medium
|
||||
|
||||
# --- Transcription Service (WhisperX ASR Endpoint) ---
|
||||
# New connector architecture auto-detects ASR mode when ASR_BASE_URL is set.
|
||||
# USE_ASR_ENDPOINT=true is deprecated but still works for backwards compatibility.
|
||||
#
|
||||
# Note: ASR endpoints handle chunking internally - CHUNK_LIMIT settings are ignored.
|
||||
|
||||
# WhisperX ASR Endpoint URL (setting this auto-enables ASR mode)
|
||||
# For containers in same docker-compose: Use container name and internal port
|
||||
# Example: http://whisperx-asr:9000 (NOT the host port or external IP)
|
||||
# For external ASR: Use http://192.168.1.100:9000 or http://asr.example.com:9000
|
||||
ASR_BASE_URL=http://whisperx-asr:9000
|
||||
|
||||
# Deprecated: No longer needed, kept for backwards compatibility
|
||||
# USE_ASR_ENDPOINT=true
|
||||
|
||||
# Speaker diarization options
|
||||
ASR_DIARIZE=true
|
||||
# ASR_MIN_SPEAKERS=1 # Hint for minimum speakers
|
||||
# ASR_MAX_SPEAKERS=5 # Default maximum speakers
|
||||
|
||||
# Enable speaker embeddings for voice profile matching (WhisperX only)
|
||||
ASR_RETURN_SPEAKER_EMBEDDINGS=true
|
||||
|
||||
# --- Application Settings ---
|
||||
# Set to "true" to allow user registration, "false" to disable
|
||||
ALLOW_REGISTRATION=false
|
||||
# Comma-separated list of allowed email domains for registration.
|
||||
# Leave empty to allow all domains. Example: company.com,subsidiary.org
|
||||
REGISTRATION_ALLOWED_DOMAINS=
|
||||
SUMMARY_MAX_TOKENS=8000
|
||||
CHAT_MAX_TOKENS=5000
|
||||
|
||||
# Timezone for displaying dates and times in the UI
|
||||
# Use a valid TZ database name (e.g., "America/New_York", "Europe/London", "UTC")
|
||||
TIMEZONE="UTC"
|
||||
|
||||
# Set the logging level for the application.
|
||||
# Options: DEBUG, INFO, WARNING, ERROR
|
||||
LOG_LEVEL="INFO"
|
||||
|
||||
# --- Audio Compression ---
|
||||
# Automatically compress lossless uploads (WAV, AIFF) to save storage
|
||||
AUDIO_COMPRESS_UPLOADS=true
|
||||
|
||||
# Target codec: mp3 (lossy, smallest), flac (lossless), opus (lossy, efficient)
|
||||
AUDIO_CODEC=mp3
|
||||
|
||||
# Bitrate for lossy codecs (ignored for FLAC)
|
||||
AUDIO_BITRATE=128k
|
||||
|
||||
# --- Admin User (created on first run) ---
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_EMAIL=admin@example.com
|
||||
ADMIN_PASSWORD=changeme
|
||||
|
||||
# --- Inquire Mode (AI search across all recordings) ---
|
||||
# Set to "true" to enable semantic search and chat across all recordings
|
||||
# Requires additional dependencies (already included in Docker image)
|
||||
ENABLE_INQUIRE_MODE=false
|
||||
|
||||
# --- Automated File Processing (Black Hole Directory) ---
|
||||
# Set to "true" to enable automated file processing
|
||||
ENABLE_AUTO_PROCESSING=false
|
||||
|
||||
# --- Automated Export Settings ---
|
||||
# Automatically export transcriptions and summaries to markdown files
|
||||
ENABLE_AUTO_EXPORT=false
|
||||
|
||||
# Directory where exports will be saved (per-user subdirectories created automatically)
|
||||
AUTO_EXPORT_DIR=/data/exports
|
||||
|
||||
# What to include in exports
|
||||
AUTO_EXPORT_TRANSCRIPTION=true
|
||||
AUTO_EXPORT_SUMMARY=true
|
||||
|
||||
# Processing mode: admin_only, user_directories, or single_user
|
||||
AUTO_PROCESS_MODE=admin_only
|
||||
|
||||
# Directory to watch for new audio files
|
||||
AUTO_PROCESS_WATCH_DIR=/data/auto-process
|
||||
|
||||
# How often to check for new files (seconds)
|
||||
AUTO_PROCESS_CHECK_INTERVAL=30
|
||||
|
||||
# How long to wait (seconds) to confirm a file has stopped changing before processing.
|
||||
# Increase for slow network transfers (NFS, SMB). Default: 5
|
||||
# AUTO_PROCESS_STABILITY_TIME=5
|
||||
|
||||
# Default username for single_user mode (only used if AUTO_PROCESS_MODE=single_user)
|
||||
# AUTO_PROCESS_DEFAULT_USERNAME=admin
|
||||
|
||||
# --- Auto-Deletion & Retention Settings ---
|
||||
# Enable automated deletion of old recordings
|
||||
ENABLE_AUTO_DELETION=false
|
||||
|
||||
# Number of days to retain recordings (0 = disabled)
|
||||
# Example: 90 means recordings older than 90 days will be processed
|
||||
GLOBAL_RETENTION_DAYS=90
|
||||
|
||||
# Deletion mode: 'audio_only' keeps transcription, 'full_recording' deletes everything
|
||||
# audio_only: Deletes audio file but keeps transcription/summary/notes (recommended)
|
||||
# full_recording: Permanently deletes the entire recording from database
|
||||
DELETION_MODE=audio_only
|
||||
|
||||
# --- Permission-Based Deletion Controls ---
|
||||
# Allow all users to delete their recordings, or restrict to admins only
|
||||
# true: All users can delete their own recordings (default)
|
||||
# false: Only admins can delete recordings
|
||||
USERS_CAN_DELETE=true
|
||||
|
||||
# Delete speaker profiles when all their recordings are removed.
|
||||
# Default: false (speaker profiles and voice embeddings are preserved)
|
||||
# Set to true for privacy-sensitive deployments where biometric voice data
|
||||
# should not outlive the recordings it was derived from.
|
||||
# DELETE_ORPHANED_SPEAKERS=false
|
||||
|
||||
# --- Internal Sharing Settings ---
|
||||
# Enable user-to-user sharing of recordings (works independently of groups)
|
||||
ENABLE_INTERNAL_SHARING=false
|
||||
|
||||
# Show usernames in the UI (when sharing/viewing shared recordings)
|
||||
# true: Display usernames throughout the interface
|
||||
# false: Hide usernames (users must know each other's usernames to share)
|
||||
SHOW_USERNAMES_IN_UI=false
|
||||
|
||||
# --- Public Sharing Settings ---
|
||||
# Enable creation of public share links (anonymous access)
|
||||
# true: Users can create public links to share recordings externally (default)
|
||||
# false: Public sharing is disabled globally
|
||||
ENABLE_PUBLIC_SHARING=true
|
||||
|
||||
# Note: Admins can control public sharing permissions per-user in the admin dashboard
|
||||
# even when ENABLE_PUBLIC_SHARING is true
|
||||
|
||||
# --- Video Retention ---
|
||||
# When enabled, uploaded video files keep their video stream for in-browser playback
|
||||
# The audio is extracted to a temp file for transcription, then cleaned up
|
||||
# Default: false (video uploads extract audio only, video stream is discarded)
|
||||
VIDEO_RETENTION=false
|
||||
|
||||
# --- Concurrent Uploads ---
|
||||
# Maximum number of simultaneous file uploads (default: 3)
|
||||
MAX_CONCURRENT_UPLOADS=3
|
||||
|
||||
# --- Background Processing Queues ---
|
||||
# Separate queues for transcription (slow) and summary (fast) jobs
|
||||
# This prevents slow ASR jobs from blocking quick summary generation
|
||||
|
||||
# Transcription queue workers (for ASR processing, default: 2)
|
||||
JOB_QUEUE_WORKERS=2
|
||||
|
||||
# Summary queue workers (for LLM summarization, default: 2)
|
||||
SUMMARY_QUEUE_WORKERS=2
|
||||
|
||||
# Maximum retry attempts for failed jobs (default: 3)
|
||||
JOB_MAX_RETRIES=3
|
||||
|
||||
# --- Docker Settings (rarely need to be changed) ---
|
||||
# Database URI - SQLite (default) or PostgreSQL
|
||||
SQLALCHEMY_DATABASE_URI=sqlite:////data/instance/transcriptions.db
|
||||
# For PostgreSQL, use: postgresql://username:password@hostname:5432/database_name
|
||||
# Example: postgresql://speakr:password@postgres:5432/speakr
|
||||
UPLOAD_FOLDER=/data/uploads
|
||||
1
constraints.txt
Normal file
1
constraints.txt
Normal file
@@ -0,0 +1 @@
|
||||
scipy<1.15
|
||||
105
deployment/README.md
Normal file
105
deployment/README.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# DictIA — Deployment Infrastructure
|
||||
|
||||
Infrastructure de deploiement reproductible pour DictIA .
|
||||
|
||||
## Choix de profil
|
||||
|
||||
```
|
||||
Quel est ton setup?
|
||||
|
|
||||
+-- VPS / serveur cloud?
|
||||
| --> cloud (ASR Proxy GCP GPU on demand)
|
||||
|
|
||||
+-- Machine locale avec GPU NVIDIA?
|
||||
| --> local-gpu (WhisperX sur GPU, le plus rapide)
|
||||
|
|
||||
+-- Machine locale sans GPU?
|
||||
--> local-cpu (WhisperX sur CPU, lent mais fonctionnel)
|
||||
```
|
||||
|
||||
## Quickstart
|
||||
|
||||
```bash
|
||||
git clone https://gitea.innova-ai.ca/Innova-AI/dictia-public.git
|
||||
cd dictia
|
||||
git checkout dictia-branding
|
||||
bash deployment/setup.sh
|
||||
```
|
||||
|
||||
Le script detecte le hardware et guide l'installation.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
deployment/
|
||||
├── setup.sh # Installateur principal
|
||||
├── docker/
|
||||
│ ├── docker-compose.cloud.yml
|
||||
│ ├── docker-compose.local-cpu.yml
|
||||
│ ├── docker-compose.local-gpu.yml
|
||||
│ └── .env.example
|
||||
├── asr-proxy/ # Proxy GCP GPU (cloud seulement)
|
||||
│ ├── proxy.py
|
||||
│ ├── dashboard.html
|
||||
│ ├── requirements.txt
|
||||
│ ├── setup.sh
|
||||
│ └── asr-proxy.service
|
||||
├── security/ # Securite Docker (cloud)
|
||||
│ ├── docker-daemon.json
|
||||
│ ├── iptables-rules.sh
|
||||
│ └── docker-iptables.service
|
||||
├── config/
|
||||
│ ├── nginx/dictia.conf
|
||||
│ ├── tailscale/setup-serve.sh
|
||||
│ └── systemd/dictia.service
|
||||
├── tools/
|
||||
│ ├── backup.sh
|
||||
│ ├── restore.sh
|
||||
│ ├── update.sh
|
||||
│ └── health-check.sh
|
||||
└── docs/
|
||||
├── QUICKSTART.md
|
||||
├── VPS-SETUP.md
|
||||
├── LOCAL-SETUP.md
|
||||
├── MAINTENANCE.md
|
||||
└── TROUBLESHOOTING.md
|
||||
```
|
||||
|
||||
### Profil Cloud
|
||||
|
||||
```
|
||||
Internet --> Tailscale --> VPS
|
||||
|
|
||||
DictIA :8899
|
||||
|
|
||||
ASR Proxy :9090
|
||||
|
|
||||
GCP GPU (auto start/stop)
|
||||
|
|
||||
WhisperX :9000
|
||||
```
|
||||
|
||||
### Profil Local GPU/CPU
|
||||
|
||||
```
|
||||
localhost:8899 --> DictIA container
|
||||
|
|
||||
WhisperX container :9000
|
||||
|
|
||||
GPU local (ou CPU)
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- [QUICKSTART.md](docs/QUICKSTART.md) — Demarrage rapide par profil
|
||||
- [VPS-SETUP.md](docs/VPS-SETUP.md) — Setup VPS complet from scratch
|
||||
- [LOCAL-SETUP.md](docs/LOCAL-SETUP.md) — Setup local GPU/CPU
|
||||
- [MAINTENANCE.md](docs/MAINTENANCE.md) — Backup, restore, update, monitoring
|
||||
- [TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) — Problemes courants + solutions
|
||||
|
||||
## Mise a jour upstream
|
||||
|
||||
Tous les fichiers dans `deployment/` sont specifiques a DictIA.
|
||||
Aucun conflit lors des merges upstream, sauf `deployment/setup.sh`
|
||||
(qui remplace le setup.sh original de Speakr).
|
||||
|
||||
5
deployment/asr-proxy/.gitignore
vendored
Normal file
5
deployment/asr-proxy/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
gcp-credentials.json
|
||||
usage-stats.json
|
||||
venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
22
deployment/asr-proxy/asr-proxy.service
Normal file
22
deployment/asr-proxy/asr-proxy.service
Normal file
@@ -0,0 +1,22 @@
|
||||
# TEMPLATE — Ne pas copier directement dans /etc/systemd/system/.
|
||||
# Les variables ${ASR_PROXY_USER} et ${ASR_PROXY_DIR} sont des placeholders.
|
||||
# Le fichier service réel est généré par setup.sh (via heredoc bash) avec les
|
||||
# valeurs résolues de $SERVICE_USER et $INSTALL_DIR.
|
||||
# Usage : sudo bash setup.sh (installe et active le service automatiquement)
|
||||
|
||||
[Unit]
|
||||
Description=DictIA ASR Proxy - GPU Auto-Start/Stop for WhisperX
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=${ASR_PROXY_USER}
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
WorkingDirectory=${ASR_PROXY_DIR}
|
||||
ExecStart=${ASR_PROXY_DIR}/venv/bin/python proxy.py
|
||||
Environment=GOOGLE_APPLICATION_CREDENTIALS=${ASR_PROXY_DIR}/gcp-credentials.json
|
||||
Environment=STATS_FILE=${ASR_PROXY_DIR}/usage-stats.json
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
1534
deployment/asr-proxy/dashboard.html
Normal file
1534
deployment/asr-proxy/dashboard.html
Normal file
File diff suppressed because it is too large
Load Diff
741
deployment/asr-proxy/proxy.py
Normal file
741
deployment/asr-proxy/proxy.py
Normal file
@@ -0,0 +1,741 @@
|
||||
"""DictIA ASR Proxy - Auto-start/stop GCP GPU for WhisperX + Ollama.
|
||||
|
||||
Uses Google Cloud Compute REST API directly (no gcloud CLI needed).
|
||||
Proxies both ASR (WhisperX) and LLM (Ollama) requests.
|
||||
Multi-zone fallback across Canada (Montreal + Toronto).
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
|
||||
import httpx
|
||||
import jwt as pyjwt
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import HTMLResponse, JSONResponse, Response
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
||||
log = logging.getLogger("asr-proxy")
|
||||
|
||||
# Config — paths relative to this script's directory by default
|
||||
SCRIPT_DIR = Path(__file__).parent
|
||||
GCP_PROJECT = os.getenv("GCP_PROJECT", "speakr-gpu")
|
||||
WHISPERX_PORT = int(os.getenv("WHISPERX_PORT", "9000"))
|
||||
OLLAMA_PORT = int(os.getenv("OLLAMA_PORT", "11434"))
|
||||
IDLE_TIMEOUT = int(os.getenv("IDLE_TIMEOUT", "300"))
|
||||
CREDS_FILE = os.getenv("GOOGLE_APPLICATION_CREDENTIALS", str(SCRIPT_DIR / "gcp-credentials.json"))
|
||||
STATS_FILE = os.getenv("STATS_FILE", str(SCRIPT_DIR / "usage-stats.json"))
|
||||
MONTHLY_LIMIT_HOURS = float(os.getenv("MONTHLY_LIMIT_HOURS", "30"))
|
||||
# Real GCP cost per GPU-hour (g2-standard-4 + L4): GPU ($0.837) + vCPU ($0.151) + RAM ($0.069)
|
||||
GPU_COST_PER_HOUR = float(os.getenv("GPU_COST_PER_HOUR", "1.06"))
|
||||
# Fixed monthly costs: SSD disks ($5.66) + snapshots ($4.19) ≈ $9.85/month
|
||||
FIXED_MONTHLY_COST = float(os.getenv("FIXED_MONTHLY_COST", "9.85"))
|
||||
SNAPSHOT_NAME = "whisperx-gpu-snapshot"
|
||||
HEALTH_POLL_INTERVAL = 5
|
||||
BOOT_TIMEOUT = 300
|
||||
|
||||
# Zone fallback order — Canada only, Montreal first
|
||||
ZONE_FALLBACKS = [
|
||||
{
|
||||
"zone": "northamerica-northeast1-b",
|
||||
"instance": "whisperx-gpu-mtl1",
|
||||
"machine_type": "g2-standard-4",
|
||||
"accelerator": "nvidia-l4",
|
||||
"accel_count": 1,
|
||||
"label": "Montreal-b (L4)",
|
||||
},
|
||||
{
|
||||
"zone": "northamerica-northeast1-c",
|
||||
"instance": "whisperx-gpu-mtl2",
|
||||
"machine_type": "n1-standard-4",
|
||||
"accelerator": "nvidia-tesla-t4",
|
||||
"accel_count": 1,
|
||||
"label": "Montreal-c (T4)",
|
||||
},
|
||||
{
|
||||
"zone": "northamerica-northeast2-a",
|
||||
"instance": "whisperx-gpu-tor1",
|
||||
"machine_type": "g2-standard-4",
|
||||
"accelerator": "nvidia-l4",
|
||||
"accel_count": 1,
|
||||
"label": "Toronto-a (L4)",
|
||||
},
|
||||
{
|
||||
"zone": "northamerica-northeast2-b",
|
||||
"instance": "whisperx-gpu",
|
||||
"machine_type": "g2-standard-4",
|
||||
"accelerator": "nvidia-l4",
|
||||
"accel_count": 1,
|
||||
"label": "Toronto-b (L4)",
|
||||
},
|
||||
]
|
||||
|
||||
STARTUP_SCRIPT = """#!/bin/bash
|
||||
systemctl start docker
|
||||
sleep 5
|
||||
docker start whisperx-asr 2>/dev/null || true
|
||||
systemctl start ollama 2>/dev/null || true
|
||||
"""
|
||||
|
||||
app = FastAPI(title="DictIA ASR Proxy")
|
||||
|
||||
# State
|
||||
last_request_time = 0.0
|
||||
active_requests = 0
|
||||
gpu_ip: str | None = None
|
||||
active_zone: dict | None = None
|
||||
shutdown_task: asyncio.Task | None = None
|
||||
|
||||
# Request history tracking (in-memory, last 20 requests)
|
||||
request_history: list[dict] = []
|
||||
MAX_HISTORY = 20
|
||||
|
||||
# Zone status tracking
|
||||
zone_status: dict[str, dict] = {}
|
||||
|
||||
# Startup lock and failure cooldown
|
||||
_startup_lock: asyncio.Lock | None = None
|
||||
_last_failure_time: float = 0
|
||||
FAILURE_COOLDOWN = 180
|
||||
|
||||
# OAuth2 token cache
|
||||
_access_token: str | None = None
|
||||
_token_expiry: float = 0
|
||||
|
||||
|
||||
# --- Usage Stats ---
|
||||
|
||||
def load_stats() -> dict:
|
||||
try:
|
||||
with open(STATS_FILE) as f:
|
||||
return json.load(f)
|
||||
except (FileNotFoundError, json.JSONDecodeError):
|
||||
return {"gpu_seconds": 0, "month": time.strftime("%Y-%m"), "requests": 0, "last_start": 0}
|
||||
|
||||
|
||||
def save_stats(stats: dict):
|
||||
with open(STATS_FILE, "w") as f:
|
||||
json.dump(stats, f, indent=2)
|
||||
|
||||
|
||||
def track_gpu_time():
|
||||
stats = load_stats()
|
||||
current_month = time.strftime("%Y-%m")
|
||||
if stats.get("month") != current_month:
|
||||
stats = {"gpu_seconds": 0, "month": current_month, "requests": 0, "last_start": 0}
|
||||
if stats.get("last_start", 0) > 0:
|
||||
elapsed = time.time() - stats["last_start"]
|
||||
stats["gpu_seconds"] += elapsed
|
||||
stats["last_start"] = 0
|
||||
save_stats(stats)
|
||||
|
||||
|
||||
def check_budget() -> tuple[bool, float]:
|
||||
stats = load_stats()
|
||||
current_month = time.strftime("%Y-%m")
|
||||
if stats.get("month") != current_month:
|
||||
return True, 0.0
|
||||
hours_used = stats.get("gpu_seconds", 0) / 3600
|
||||
return hours_used < MONTHLY_LIMIT_HOURS, hours_used
|
||||
|
||||
|
||||
# --- GCP Auth ---
|
||||
|
||||
async def get_access_token() -> str:
|
||||
global _access_token, _token_expiry
|
||||
if _access_token and time.time() < _token_expiry - 60:
|
||||
return _access_token
|
||||
with open(CREDS_FILE) as f:
|
||||
creds = json.load(f)
|
||||
cred_type = creds.get("type", "authorized_user")
|
||||
async with httpx.AsyncClient() as client:
|
||||
if cred_type == "service_account":
|
||||
now = int(time.time())
|
||||
payload = {
|
||||
"iss": creds["client_email"],
|
||||
"scope": "https://www.googleapis.com/auth/compute",
|
||||
"aud": "https://oauth2.googleapis.com/token",
|
||||
"iat": now,
|
||||
"exp": now + 3600,
|
||||
}
|
||||
signed = pyjwt.encode(payload, creds["private_key"], algorithm="RS256")
|
||||
resp = await client.post(
|
||||
"https://oauth2.googleapis.com/token",
|
||||
data={
|
||||
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
||||
"assertion": signed,
|
||||
},
|
||||
)
|
||||
else:
|
||||
resp = await client.post(
|
||||
"https://oauth2.googleapis.com/token",
|
||||
data={
|
||||
"client_id": creds["client_id"],
|
||||
"client_secret": creds["client_secret"],
|
||||
"refresh_token": creds["refresh_token"],
|
||||
"grant_type": "refresh_token",
|
||||
},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
_access_token = data["access_token"]
|
||||
_token_expiry = time.time() + data.get("expires_in", 3600)
|
||||
log.info(f"Refreshed GCP access token ({cred_type})")
|
||||
return _access_token
|
||||
|
||||
|
||||
# --- GCP Compute API ---
|
||||
|
||||
COMPUTE_BASE = "https://compute.googleapis.com/compute/v1"
|
||||
|
||||
|
||||
async def gcp_api(method: str, url: str, **kwargs) -> httpx.Response:
|
||||
token = await get_access_token()
|
||||
async with httpx.AsyncClient(timeout=60) as client:
|
||||
resp = await client.request(
|
||||
method, url,
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
**kwargs,
|
||||
)
|
||||
return resp
|
||||
|
||||
|
||||
async def get_instance_info(zone: str, instance: str) -> dict | None:
|
||||
url = f"{COMPUTE_BASE}/projects/{GCP_PROJECT}/zones/{zone}/instances/{instance}"
|
||||
resp = await gcp_api("GET", url)
|
||||
if resp.status_code == 404:
|
||||
return None
|
||||
if resp.status_code >= 400:
|
||||
log.error(f"GCP API error {resp.status_code}: {resp.text}")
|
||||
return None
|
||||
return resp.json()
|
||||
|
||||
|
||||
def extract_ip(instance_data: dict) -> str:
|
||||
interfaces = instance_data.get("networkInterfaces", [])
|
||||
if interfaces:
|
||||
access = interfaces[0].get("accessConfigs", [])
|
||||
if access:
|
||||
return access[0].get("natIP", "")
|
||||
return ""
|
||||
|
||||
|
||||
async def start_instance_in_zone(zone: str, instance: str) -> bool:
|
||||
url = f"{COMPUTE_BASE}/projects/{GCP_PROJECT}/zones/{zone}/instances/{instance}/start"
|
||||
resp = await gcp_api("POST", url)
|
||||
if resp.status_code < 400:
|
||||
log.info(f"Start requested: {instance} in {zone}")
|
||||
return True
|
||||
log.warning(f"Failed to start {instance} in {zone}: {resp.status_code} {resp.text}")
|
||||
return False
|
||||
|
||||
|
||||
async def stop_instance_in_zone(zone: str, instance: str):
|
||||
url = f"{COMPUTE_BASE}/projects/{GCP_PROJECT}/zones/{zone}/instances/{instance}/stop"
|
||||
resp = await gcp_api("POST", url)
|
||||
if resp.status_code < 400:
|
||||
log.info(f"Stop requested: {instance} in {zone}")
|
||||
else:
|
||||
log.error(f"Failed to stop {instance} in {zone}: {resp.status_code}")
|
||||
|
||||
|
||||
async def create_instance_from_snapshot(config: dict) -> bool:
|
||||
zone = config["zone"]
|
||||
instance = config["instance"]
|
||||
machine = config["machine_type"]
|
||||
accel = config["accelerator"]
|
||||
accel_count = config["accel_count"]
|
||||
|
||||
log.info(f"Creating {instance} in {zone} from snapshot...")
|
||||
|
||||
body = {
|
||||
"name": instance,
|
||||
"machineType": f"zones/{zone}/machineTypes/{machine}",
|
||||
"disks": [{
|
||||
"boot": True,
|
||||
"autoDelete": True,
|
||||
"initializeParams": {
|
||||
"diskSizeGb": "50",
|
||||
"diskType": f"zones/{zone}/diskTypes/pd-ssd",
|
||||
"sourceSnapshot": f"global/snapshots/{SNAPSHOT_NAME}",
|
||||
},
|
||||
}],
|
||||
"networkInterfaces": [{
|
||||
"network": "global/networks/default",
|
||||
"accessConfigs": [{"type": "ONE_TO_ONE_NAT", "name": "External NAT"}],
|
||||
}],
|
||||
"guestAccelerators": [{
|
||||
"acceleratorType": f"zones/{zone}/acceleratorTypes/{accel}",
|
||||
"acceleratorCount": accel_count,
|
||||
}],
|
||||
"scheduling": {
|
||||
"onHostMaintenance": "TERMINATE",
|
||||
"automaticRestart": False,
|
||||
},
|
||||
"tags": {"items": ["whisperx-gpu"]},
|
||||
"metadata": {
|
||||
"items": [{"key": "startup-script", "value": STARTUP_SCRIPT}],
|
||||
},
|
||||
}
|
||||
|
||||
url = f"{COMPUTE_BASE}/projects/{GCP_PROJECT}/zones/{zone}/instances"
|
||||
resp = await gcp_api("POST", url, json=body)
|
||||
|
||||
if resp.status_code < 400:
|
||||
log.info(f"Created {instance} in {zone}")
|
||||
return True
|
||||
|
||||
error_text = resp.text
|
||||
if "ZONE_RESOURCE_POOL_EXHAUSTED" in error_text:
|
||||
log.warning(f"No capacity in {zone} -- skipping")
|
||||
elif "QUOTA" in error_text.upper():
|
||||
log.warning(f"Quota exceeded for {zone}: {error_text[:200]}")
|
||||
else:
|
||||
log.error(f"Failed to create in {zone}: {resp.status_code} {error_text[:200]}")
|
||||
return False
|
||||
|
||||
|
||||
# --- Core Logic ---
|
||||
|
||||
async def wait_for_running(zone: str, instance: str, timeout: int = 120, grace: int = 15) -> bool:
|
||||
gone_count = 0
|
||||
start_time = time.time()
|
||||
for _ in range(timeout // 5):
|
||||
info = await get_instance_info(zone, instance)
|
||||
if info and info.get("status") == "RUNNING":
|
||||
return True
|
||||
status = info.get("status", "UNKNOWN") if info else "GONE"
|
||||
elapsed = time.time() - start_time
|
||||
if status == "GONE":
|
||||
gone_count += 1
|
||||
if gone_count >= 2:
|
||||
log.warning(f"{instance} in {zone}: instance disappeared (no capacity)")
|
||||
return False
|
||||
if status in ("STOPPING",):
|
||||
log.warning(f"{instance} in {zone}: status {status} (no capacity)")
|
||||
return False
|
||||
if status in ("TERMINATED", "STOPPED") and elapsed > grace:
|
||||
log.warning(f"{instance} in {zone}: status {status} after {elapsed:.0f}s (no capacity)")
|
||||
return False
|
||||
await asyncio.sleep(5)
|
||||
return False
|
||||
|
||||
|
||||
async def delete_instance(zone: str, instance: str):
|
||||
url = f"{COMPUTE_BASE}/projects/{GCP_PROJECT}/zones/{zone}/instances/{instance}"
|
||||
resp = await gcp_api("DELETE", url)
|
||||
if resp.status_code < 400:
|
||||
log.info(f"Deleted {instance} in {zone} to free quota")
|
||||
elif resp.status_code == 404:
|
||||
pass
|
||||
else:
|
||||
log.warning(f"Failed to delete {instance} in {zone}: {resp.status_code}")
|
||||
|
||||
|
||||
async def ensure_gpu_running() -> str:
|
||||
global gpu_ip, active_zone, _last_failure_time
|
||||
|
||||
if _last_failure_time > 0:
|
||||
remaining = FAILURE_COOLDOWN - (time.time() - _last_failure_time)
|
||||
if remaining > 0:
|
||||
log.info(f"GPU cooldown active ({int(remaining)}s remaining), waiting...")
|
||||
await asyncio.sleep(remaining)
|
||||
_last_failure_time = 0
|
||||
|
||||
async with _startup_lock:
|
||||
ok, hours = check_budget()
|
||||
if not ok:
|
||||
raise RuntimeError(f"Monthly GPU limit reached ({hours:.1f}h / {MONTHLY_LIMIT_HOURS}h)")
|
||||
|
||||
if active_zone:
|
||||
info = await get_instance_info(active_zone["zone"], active_zone["instance"])
|
||||
if info and info.get("status") == "RUNNING":
|
||||
gpu_ip = extract_ip(info)
|
||||
if gpu_ip:
|
||||
return gpu_ip
|
||||
|
||||
errors = []
|
||||
|
||||
for config in ZONE_FALLBACKS:
|
||||
zone = config["zone"]
|
||||
instance = config["instance"]
|
||||
label = config["label"]
|
||||
|
||||
log.info(f"Trying {label}...")
|
||||
info = await get_instance_info(zone, instance)
|
||||
|
||||
if info is None:
|
||||
created = await create_instance_from_snapshot(config)
|
||||
if not created:
|
||||
zone_status[label] = {
|
||||
"status": "no_capacity",
|
||||
"last_tried": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
"last_error": "no capacity",
|
||||
}
|
||||
errors.append(f"{label}: no capacity")
|
||||
continue
|
||||
if not await wait_for_running(zone, instance, grace=30):
|
||||
zone_status[label] = {
|
||||
"status": "error",
|
||||
"last_tried": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
"last_error": "created but failed to start",
|
||||
}
|
||||
errors.append(f"{label}: created but failed to start")
|
||||
await delete_instance(zone, instance)
|
||||
await asyncio.sleep(3)
|
||||
continue
|
||||
else:
|
||||
status = info.get("status", "UNKNOWN")
|
||||
|
||||
if status == "RUNNING":
|
||||
pass
|
||||
elif status in ("TERMINATED", "STOPPED"):
|
||||
zone_status[label] = {
|
||||
"status": "starting",
|
||||
"last_tried": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
"last_error": None,
|
||||
}
|
||||
started = await start_instance_in_zone(zone, instance)
|
||||
if not started:
|
||||
zone_status[label] = {
|
||||
"status": "error",
|
||||
"last_tried": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
"last_error": "start rejected",
|
||||
}
|
||||
errors.append(f"{label}: start rejected")
|
||||
continue
|
||||
if not await wait_for_running(zone, instance, grace=20):
|
||||
zone_status[label] = {
|
||||
"status": "error",
|
||||
"last_tried": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
"last_error": "didn't reach RUNNING",
|
||||
}
|
||||
errors.append(f"{label}: didn't reach RUNNING")
|
||||
continue
|
||||
elif status in ("STAGING", "PROVISIONING"):
|
||||
zone_status[label] = {
|
||||
"status": "starting",
|
||||
"last_tried": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
"last_error": None,
|
||||
}
|
||||
if not await wait_for_running(zone, instance):
|
||||
zone_status[label] = {
|
||||
"status": "error",
|
||||
"last_tried": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
"last_error": f"stuck in {status}",
|
||||
}
|
||||
errors.append(f"{label}: stuck in {status}")
|
||||
continue
|
||||
elif status == "STOPPING":
|
||||
log.info(f"{label}: STOPPING, deleting to free quota")
|
||||
await delete_instance(zone, instance)
|
||||
await asyncio.sleep(3)
|
||||
zone_status[label] = {
|
||||
"status": "error",
|
||||
"last_tried": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
"last_error": "was STOPPING, deleted",
|
||||
}
|
||||
errors.append(f"{label}: was STOPPING, deleted")
|
||||
continue
|
||||
|
||||
info = await get_instance_info(zone, instance)
|
||||
if info and info.get("status") == "RUNNING":
|
||||
gpu_ip = extract_ip(info)
|
||||
if gpu_ip:
|
||||
active_zone = config
|
||||
_last_failure_time = 0
|
||||
zone_status[label] = {
|
||||
"status": "running",
|
||||
"last_tried": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
"last_error": None,
|
||||
}
|
||||
stats = load_stats()
|
||||
stats["last_start"] = time.time()
|
||||
stats["requests"] = stats.get("requests", 0) + 1
|
||||
stats["active_zone"] = label
|
||||
save_stats(stats)
|
||||
log.info(f"GPU ready in {label}, IP: {gpu_ip}")
|
||||
return gpu_ip
|
||||
|
||||
zone_status[label] = {
|
||||
"status": "error",
|
||||
"last_tried": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
"last_error": "running but no IP",
|
||||
}
|
||||
errors.append(f"{label}: running but no IP")
|
||||
|
||||
_last_failure_time = time.time()
|
||||
raise RuntimeError(
|
||||
f"No GPU available in any Canadian zone. Tried: {'; '.join(errors)}"
|
||||
)
|
||||
|
||||
|
||||
async def ensure_gpu_ready() -> str:
|
||||
ip = await ensure_gpu_running()
|
||||
url = f"http://{ip}:{WHISPERX_PORT}/health"
|
||||
log.info(f"Waiting for WhisperX at {url}...")
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
for _ in range(BOOT_TIMEOUT // HEALTH_POLL_INTERVAL):
|
||||
try:
|
||||
resp = await client.get(url)
|
||||
if resp.status_code == 200:
|
||||
log.info("WhisperX is healthy!")
|
||||
return ip
|
||||
except (httpx.ConnectError, httpx.ConnectTimeout, httpx.ReadTimeout):
|
||||
pass
|
||||
await asyncio.sleep(HEALTH_POLL_INTERVAL)
|
||||
raise RuntimeError("WhisperX did not become healthy in time")
|
||||
|
||||
|
||||
async def ensure_ollama_ready() -> str:
|
||||
ip = await ensure_gpu_running()
|
||||
url = f"http://{ip}:{OLLAMA_PORT}/api/tags"
|
||||
log.info(f"Waiting for Ollama at {url}...")
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
for _ in range(BOOT_TIMEOUT // HEALTH_POLL_INTERVAL):
|
||||
try:
|
||||
resp = await client.get(url)
|
||||
if resp.status_code == 200:
|
||||
log.info("Ollama is healthy!")
|
||||
return ip
|
||||
except (httpx.ConnectError, httpx.ConnectTimeout, httpx.ReadTimeout):
|
||||
pass
|
||||
await asyncio.sleep(HEALTH_POLL_INTERVAL)
|
||||
raise RuntimeError("Ollama did not become healthy in time")
|
||||
|
||||
|
||||
async def idle_shutdown_loop():
|
||||
while True:
|
||||
await asyncio.sleep(60)
|
||||
if last_request_time == 0 or active_zone is None:
|
||||
continue
|
||||
if active_requests > 0:
|
||||
continue
|
||||
elapsed = time.time() - last_request_time
|
||||
if elapsed >= IDLE_TIMEOUT:
|
||||
try:
|
||||
zone = active_zone["zone"]
|
||||
instance = active_zone["instance"]
|
||||
label = active_zone["label"]
|
||||
info = await get_instance_info(zone, instance)
|
||||
if info and info.get("status") == "RUNNING":
|
||||
log.info(f"Idle {int(elapsed)}s -- stopping {label}")
|
||||
await stop_instance_in_zone(zone, instance)
|
||||
track_gpu_time()
|
||||
except Exception as e:
|
||||
log.error(f"Error stopping: {e}")
|
||||
|
||||
|
||||
# --- Endpoints ---
|
||||
|
||||
@app.on_event("startup")
|
||||
async def on_startup():
|
||||
global shutdown_task, _startup_lock
|
||||
_startup_lock = asyncio.Lock()
|
||||
await get_access_token()
|
||||
shutdown_task = asyncio.create_task(idle_shutdown_loop())
|
||||
zones = ", ".join(c["label"] for c in ZONE_FALLBACKS)
|
||||
log.info(f"DictIA ASR Proxy started. Zones: [{zones}]. Idle: {IDLE_TIMEOUT}s, limit: {MONTHLY_LIMIT_HOURS}h")
|
||||
|
||||
|
||||
@app.post("/asr")
|
||||
async def asr_proxy(request: Request):
|
||||
global last_request_time, active_requests
|
||||
|
||||
body = await request.body()
|
||||
headers = {
|
||||
k: v for k, v in request.headers.items()
|
||||
if k.lower() not in ("host", "transfer-encoding")
|
||||
}
|
||||
|
||||
last_request_time = time.time()
|
||||
active_requests += 1
|
||||
start_time = time.time()
|
||||
result_status = 200
|
||||
try:
|
||||
ip = await ensure_gpu_ready()
|
||||
target = f"http://{ip}:{WHISPERX_PORT}/asr"
|
||||
log.info(f"Forwarding {len(body)} bytes to {target}")
|
||||
async with httpx.AsyncClient(timeout=httpx.Timeout(7200.0)) as client:
|
||||
resp = await client.post(target, content=body, headers=headers)
|
||||
last_request_time = time.time()
|
||||
result_status = resp.status_code
|
||||
ct = resp.headers.get("content-type", "")
|
||||
if "application/json" in ct:
|
||||
return JSONResponse(content=resp.json(), status_code=resp.status_code)
|
||||
else:
|
||||
return JSONResponse(content=resp.text, status_code=resp.status_code)
|
||||
except httpx.ReadTimeout:
|
||||
result_status = 504
|
||||
return JSONResponse({"error": "Transcription timeout (2h)"}, status_code=504)
|
||||
except Exception as e:
|
||||
result_status = 502
|
||||
log.error(f"Proxy error: {e}")
|
||||
return JSONResponse({"error": str(e)}, status_code=502)
|
||||
finally:
|
||||
active_requests -= 1
|
||||
last_request_time = time.time()
|
||||
request_history.insert(0, {
|
||||
"time": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
"type": "ASR",
|
||||
"duration_sec": round(time.time() - start_time, 1),
|
||||
"status": result_status,
|
||||
"zone": active_zone["label"] if active_zone else "none",
|
||||
})
|
||||
if len(request_history) > MAX_HISTORY:
|
||||
request_history.pop()
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
zone_label = active_zone["label"] if active_zone else "none"
|
||||
gpu_status = "unknown"
|
||||
if active_zone:
|
||||
try:
|
||||
info = await get_instance_info(active_zone["zone"], active_zone["instance"])
|
||||
gpu_status = info.get("status", "unknown") if info else "not_found"
|
||||
except Exception:
|
||||
pass
|
||||
ok, hours = check_budget()
|
||||
stats = load_stats()
|
||||
return {
|
||||
"proxy": "healthy",
|
||||
"gpu_instance": gpu_status,
|
||||
"gpu_zone": zone_label,
|
||||
"active_requests": active_requests,
|
||||
"idle_timeout": IDLE_TIMEOUT,
|
||||
"usage": {
|
||||
"month": stats.get("month"),
|
||||
"gpu_hours": round(hours, 2),
|
||||
"gpu_limit_hours": MONTHLY_LIMIT_HOURS,
|
||||
"requests_count": stats.get("requests", 0),
|
||||
"budget_ok": ok,
|
||||
},
|
||||
"gpu_ip": gpu_ip,
|
||||
"machine_type": active_zone.get("machine_type", "unknown") if active_zone else "unknown",
|
||||
"gpu_model": active_zone.get("accelerator", "unknown") if active_zone else "unknown",
|
||||
"idle_seconds": round(time.time() - last_request_time) if last_request_time > 0 else 0,
|
||||
"auto_shutdown_in": max(0, IDLE_TIMEOUT - round(time.time() - last_request_time)) if last_request_time > 0 and active_zone else None,
|
||||
"token_expires_in": round(_token_expiry - time.time()) if _token_expiry > 0 else None,
|
||||
}
|
||||
|
||||
|
||||
@app.get("/stats")
|
||||
async def get_stats():
|
||||
stats = load_stats()
|
||||
hours = stats.get("gpu_seconds", 0) / 3600
|
||||
gpu_cost = hours * GPU_COST_PER_HOUR
|
||||
total_cost = gpu_cost + FIXED_MONTHLY_COST
|
||||
return {
|
||||
"month": stats.get("month"),
|
||||
"gpu_hours": round(hours, 2),
|
||||
"gpu_minutes": round(hours * 60, 1),
|
||||
"estimated_cost_usd": round(total_cost, 2),
|
||||
"gpu_cost_usd": round(gpu_cost, 2),
|
||||
"fixed_cost_usd": FIXED_MONTHLY_COST,
|
||||
"monthly_limit_hours": MONTHLY_LIMIT_HOURS,
|
||||
"remaining_hours": round(MONTHLY_LIMIT_HOURS - hours, 2),
|
||||
"requests_count": stats.get("requests", 0),
|
||||
"active_zone": stats.get("active_zone", "none"),
|
||||
"cost_per_hour": GPU_COST_PER_HOUR,
|
||||
"recent_requests": request_history[:10],
|
||||
"zone_fallbacks": [
|
||||
{
|
||||
"label": config["label"],
|
||||
"zone": config["zone"],
|
||||
"machine": config["machine_type"],
|
||||
"gpu": config["accelerator"],
|
||||
**zone_status.get(config["label"], {"status": "unknown", "last_tried": None, "last_error": None}),
|
||||
}
|
||||
for config in ZONE_FALLBACKS
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@app.post("/gpu/start")
|
||||
async def gpu_start():
|
||||
try:
|
||||
ip = await ensure_gpu_ready()
|
||||
label = active_zone["label"] if active_zone else "unknown"
|
||||
return {"status": "running", "ip": ip, "zone": label}
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": str(e)}, status_code=503)
|
||||
|
||||
|
||||
@app.post("/gpu/stop")
|
||||
async def gpu_stop():
|
||||
if not active_zone:
|
||||
return {"status": "no active instance"}
|
||||
try:
|
||||
await stop_instance_in_zone(active_zone["zone"], active_zone["instance"])
|
||||
track_gpu_time()
|
||||
return {"status": "stopped", "zone": active_zone["label"]}
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": str(e)}, status_code=500)
|
||||
|
||||
|
||||
DASHBOARD_HTML = Path(__file__).parent / "dashboard.html"
|
||||
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def dashboard():
|
||||
if DASHBOARD_HTML.exists():
|
||||
return HTMLResponse(DASHBOARD_HTML.read_text(encoding="utf-8"))
|
||||
return HTMLResponse("<h1>Dashboard not found</h1><p>Place dashboard.html next to proxy.py</p>", status_code=404)
|
||||
|
||||
|
||||
@app.api_route("/v1/{path:path}", methods=["POST", "GET"])
|
||||
async def llm_proxy(request: Request, path: str):
|
||||
global last_request_time, active_requests
|
||||
|
||||
body = await request.body()
|
||||
headers = {
|
||||
k: v for k, v in request.headers.items()
|
||||
if k.lower() not in ("host", "transfer-encoding")
|
||||
}
|
||||
|
||||
last_request_time = time.time()
|
||||
active_requests += 1
|
||||
start_time = time.time()
|
||||
result_status = 200
|
||||
try:
|
||||
ip = await ensure_ollama_ready()
|
||||
target = f"http://{ip}:{OLLAMA_PORT}/v1/{path}"
|
||||
log.info(f"Forwarding LLM request to {target}")
|
||||
async with httpx.AsyncClient(timeout=httpx.Timeout(300.0)) as client:
|
||||
resp = await client.request(request.method, target, content=body, headers=headers)
|
||||
last_request_time = time.time()
|
||||
result_status = resp.status_code
|
||||
return Response(
|
||||
content=resp.content,
|
||||
status_code=resp.status_code,
|
||||
media_type=resp.headers.get("content-type"),
|
||||
)
|
||||
except httpx.ReadTimeout:
|
||||
result_status = 504
|
||||
return JSONResponse({"error": "LLM timeout (5min)"}, status_code=504)
|
||||
except Exception as e:
|
||||
result_status = 502
|
||||
log.error(f"LLM proxy error: {e}")
|
||||
return JSONResponse({"error": str(e)}, status_code=502)
|
||||
finally:
|
||||
active_requests -= 1
|
||||
last_request_time = time.time()
|
||||
request_history.insert(0, {
|
||||
"time": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
"type": "LLM",
|
||||
"duration_sec": round(time.time() - start_time, 1),
|
||||
"status": result_status,
|
||||
"zone": active_zone["label"] if active_zone else "none",
|
||||
})
|
||||
if len(request_history) > MAX_HISTORY:
|
||||
request_history.pop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=9090)
|
||||
5
deployment/asr-proxy/requirements.txt
Normal file
5
deployment/asr-proxy/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
fastapi==0.115.0
|
||||
uvicorn==0.30.0
|
||||
httpx==0.27.0
|
||||
PyJWT==2.9.0
|
||||
cryptography>=43.0.0
|
||||
87
deployment/asr-proxy/setup.sh
Normal file
87
deployment/asr-proxy/setup.sh
Normal file
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env bash
|
||||
# DictIA ASR Proxy — Setup script
|
||||
# Installs the GCP GPU proxy for cloud deployments.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
INSTALL_DIR="${ASR_PROXY_DIR:-$SCRIPT_DIR}"
|
||||
SERVICE_USER="${ASR_PROXY_USER:-$(whoami)}"
|
||||
|
||||
echo "=== DictIA ASR Proxy Setup ==="
|
||||
echo "Install directory: $INSTALL_DIR"
|
||||
echo "Service user: $SERVICE_USER"
|
||||
echo
|
||||
|
||||
# 1. Create virtual environment
|
||||
if [ ! -d "$INSTALL_DIR/venv" ]; then
|
||||
echo "[1/4] Creating Python virtual environment..."
|
||||
python3 -m venv "$INSTALL_DIR/venv"
|
||||
else
|
||||
echo "[1/4] Virtual environment already exists."
|
||||
fi
|
||||
|
||||
# 2. Install dependencies
|
||||
echo "[2/4] Installing Python dependencies..."
|
||||
"$INSTALL_DIR/venv/bin/pip" install --quiet --upgrade pip
|
||||
"$INSTALL_DIR/venv/bin/pip" install --quiet -r "$INSTALL_DIR/requirements.txt"
|
||||
|
||||
# 3. GCP credentials
|
||||
if [ ! -f "$INSTALL_DIR/gcp-credentials.json" ]; then
|
||||
echo "[3/4] GCP credentials not found."
|
||||
echo " Place your GCP service account or OAuth credentials at:"
|
||||
echo " $INSTALL_DIR/gcp-credentials.json"
|
||||
echo
|
||||
echo " For service account: download JSON from GCP Console > IAM > Service Accounts"
|
||||
echo " For user credentials: run 'gcloud auth application-default login' and copy the file"
|
||||
echo
|
||||
read -rp " Path to credentials file (or press Enter to skip): " CREDS_PATH
|
||||
if [ -n "$CREDS_PATH" ] && [ -f "$CREDS_PATH" ]; then
|
||||
cp "$CREDS_PATH" "$INSTALL_DIR/gcp-credentials.json"
|
||||
chmod 600 "$INSTALL_DIR/gcp-credentials.json"
|
||||
echo " Credentials copied."
|
||||
else
|
||||
echo " Skipped. You must add credentials before starting the proxy."
|
||||
fi
|
||||
else
|
||||
echo "[3/4] GCP credentials found."
|
||||
fi
|
||||
|
||||
# 4. Install systemd service
|
||||
echo "[4/4] Installing systemd service..."
|
||||
SERVICE_FILE="/etc/systemd/system/asr-proxy.service"
|
||||
|
||||
cat > /tmp/asr-proxy.service <<UNIT
|
||||
[Unit]
|
||||
Description=DictIA ASR Proxy - GPU Auto-Start/Stop for WhisperX
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=$SERVICE_USER
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
WorkingDirectory=$INSTALL_DIR
|
||||
ExecStart=$INSTALL_DIR/venv/bin/python proxy.py
|
||||
Environment=GOOGLE_APPLICATION_CREDENTIALS=$INSTALL_DIR/gcp-credentials.json
|
||||
Environment=STATS_FILE=$INSTALL_DIR/usage-stats.json
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
UNIT
|
||||
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
cp /tmp/asr-proxy.service "$SERVICE_FILE"
|
||||
systemctl daemon-reload
|
||||
systemctl enable asr-proxy.service
|
||||
echo " Service installed and enabled."
|
||||
echo " Start with: systemctl start asr-proxy"
|
||||
else
|
||||
echo " Run as root to install systemd service, or copy manually:"
|
||||
echo " sudo cp /tmp/asr-proxy.service $SERVICE_FILE"
|
||||
echo " sudo systemctl daemon-reload && sudo systemctl enable asr-proxy"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "=== Setup complete ==="
|
||||
echo "Dashboard: http://localhost:9090"
|
||||
echo "Health: http://localhost:9090/health"
|
||||
83
deployment/config/nginx/dictia.conf
Normal file
83
deployment/config/nginx/dictia.conf
Normal file
@@ -0,0 +1,83 @@
|
||||
# DictIA — Nginx reverse proxy configuration
|
||||
#
|
||||
# Alternative to Tailscale Serve for exposing DictIA over HTTPS.
|
||||
# Replace YOUR_DOMAIN with your actual domain name.
|
||||
#
|
||||
# Install: sudo cp dictia.conf /etc/nginx/sites-available/dictia
|
||||
# sudo ln -s /etc/nginx/sites-available/dictia /etc/nginx/sites-enabled/
|
||||
# sudo nginx -t && sudo systemctl reload nginx
|
||||
#
|
||||
# For HTTPS with Let's Encrypt:
|
||||
# sudo certbot --nginx -d YOUR_DOMAIN
|
||||
|
||||
upstream dictia_app {
|
||||
server 127.0.0.1:8899;
|
||||
}
|
||||
|
||||
upstream asr_proxy {
|
||||
server 127.0.0.1:9090;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name YOUR_DOMAIN;
|
||||
|
||||
# Redirect HTTP to HTTPS (uncomment after certbot setup)
|
||||
# return 301 https://$host$request_uri;
|
||||
|
||||
client_max_body_size 500M;
|
||||
|
||||
# DictIA app
|
||||
location / {
|
||||
proxy_pass http://dictia_app;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# WebSocket support (for real-time features)
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
# Long timeouts for transcription uploads
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_connect_timeout 60s;
|
||||
}
|
||||
|
||||
# ASR Proxy dashboard (optional, restrict access)
|
||||
location /asr-proxy/ {
|
||||
proxy_pass http://asr_proxy/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
|
||||
# HTTPS server block (managed by certbot, uncomment after setup)
|
||||
# server {
|
||||
# listen 443 ssl;
|
||||
# server_name YOUR_DOMAIN;
|
||||
#
|
||||
# ssl_certificate /etc/letsencrypt/live/YOUR_DOMAIN/fullchain.pem;
|
||||
# ssl_certificate_key /etc/letsencrypt/live/YOUR_DOMAIN/privkey.pem;
|
||||
# include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
# ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||
#
|
||||
# client_max_body_size 500M;
|
||||
#
|
||||
# location / {
|
||||
# proxy_pass http://dictia_app;
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# proxy_http_version 1.1;
|
||||
# proxy_set_header Upgrade $http_upgrade;
|
||||
# proxy_set_header Connection "upgrade";
|
||||
# proxy_read_timeout 3600s;
|
||||
# proxy_send_timeout 3600s;
|
||||
# }
|
||||
# }
|
||||
15
deployment/config/systemd/dictia.service
Normal file
15
deployment/config/systemd/dictia.service
Normal file
@@ -0,0 +1,15 @@
|
||||
[Unit]
|
||||
Description=DictIA - Docker Compose Application
|
||||
After=docker.service
|
||||
Requires=docker.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
WorkingDirectory=/opt/dictia
|
||||
ExecStart=/usr/bin/docker compose -f deployment/docker/docker-compose.cloud.yml up -d
|
||||
ExecStop=/usr/bin/docker compose -f deployment/docker/docker-compose.cloud.yml down
|
||||
TimeoutStartSec=120
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
52
deployment/config/tailscale/setup-serve.sh
Normal file
52
deployment/config/tailscale/setup-serve.sh
Normal file
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
# DictIA — Tailscale Serve/Funnel setup
|
||||
#
|
||||
# Exposes DictIA and ASR Proxy dashboard via Tailscale HTTPS.
|
||||
# Based on the VPS production configuration.
|
||||
#
|
||||
# Usage:
|
||||
# bash setup-serve.sh [serve|funnel]
|
||||
# serve — accessible only within your tailnet (default)
|
||||
# funnel — accessible from the public internet
|
||||
set -euo pipefail
|
||||
|
||||
MODE="${1:-serve}"
|
||||
|
||||
echo "=== DictIA Tailscale Setup ==="
|
||||
echo "Mode: $MODE"
|
||||
echo
|
||||
|
||||
# Verify Tailscale is connected
|
||||
if ! tailscale status >/dev/null 2>&1; then
|
||||
echo "ERROR: Tailscale is not running or not connected."
|
||||
echo " Install: curl -fsSL https://tailscale.com/install.sh | sh"
|
||||
echo " Connect: sudo tailscale up"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
HOSTNAME=$(tailscale status --json | python3 -c "import sys,json; print(json.load(sys.stdin)['Self']['DNSName'].rstrip('.'))" 2>/dev/null || echo "unknown")
|
||||
echo "Tailscale hostname: $HOSTNAME"
|
||||
echo
|
||||
|
||||
# DictIA app on :443 → localhost:8899
|
||||
echo "[1/2] Setting up DictIA app (port 443 → 8899)..."
|
||||
if [ "$MODE" = "funnel" ]; then
|
||||
tailscale funnel --bg --https=443 http://localhost:8899
|
||||
else
|
||||
tailscale serve --bg --https=443 http://localhost:8899
|
||||
fi
|
||||
|
||||
# ASR Proxy dashboard on :9443 → localhost:9090
|
||||
echo "[2/2] Setting up ASR Proxy dashboard (port 9443 → 9090)..."
|
||||
if [ "$MODE" = "funnel" ]; then
|
||||
tailscale funnel --bg --https=9443 http://localhost:9090
|
||||
else
|
||||
tailscale serve --bg --https=9443 http://localhost:9090
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "=== Setup complete ==="
|
||||
echo "DictIA: https://$HOSTNAME/"
|
||||
echo "ASR Dashboard: https://$HOSTNAME:9443/"
|
||||
echo
|
||||
echo "Verify with: tailscale serve status"
|
||||
124
deployment/docker/.env.example
Normal file
124
deployment/docker/.env.example
Normal file
@@ -0,0 +1,124 @@
|
||||
# =============================================================================
|
||||
# DictIA — Unified Environment Configuration
|
||||
# =============================================================================
|
||||
#
|
||||
# Copy this file to the project root as .env and edit the values.
|
||||
# cp deployment/docker/.env.example .env
|
||||
#
|
||||
# This template combines upstream settings with DictIA deployment vars.
|
||||
# See: config/env.transcription.example for full upstream documentation.
|
||||
|
||||
# =============================================================================
|
||||
# FLASK SECRET KEY (REQUIRED — auto-generated by setup.sh)
|
||||
# =============================================================================
|
||||
SECRET_KEY=change-me-to-a-random-string
|
||||
|
||||
# =============================================================================
|
||||
# DEPLOYMENT PROFILE (used by deployment scripts)
|
||||
# =============================================================================
|
||||
# Options: cloud, local-cpu, local-gpu
|
||||
DICTIA_PROFILE=cloud
|
||||
|
||||
# =============================================================================
|
||||
# TEXT GENERATION MODEL (REQUIRED for summaries, titles, chat)
|
||||
# =============================================================================
|
||||
TEXT_MODEL_BASE_URL=https://openrouter.ai/api/v1
|
||||
TEXT_MODEL_API_KEY=your_openrouter_api_key
|
||||
TEXT_MODEL_NAME=openai/gpt-4o-mini
|
||||
|
||||
# =============================================================================
|
||||
# TRANSCRIPTION CONFIGURATION
|
||||
# =============================================================================
|
||||
# For cloud profile (ASR Proxy → GCP GPU):
|
||||
# ASR_BASE_URL is set automatically in docker-compose.cloud.yml
|
||||
# No need to set it here.
|
||||
#
|
||||
# For local profiles (WhisperX sidecar):
|
||||
# ASR_BASE_URL is set automatically in docker-compose.local-*.yml
|
||||
# No need to set it here.
|
||||
#
|
||||
# For OpenAI API instead of self-hosted ASR:
|
||||
# TRANSCRIPTION_API_KEY=sk-your_openai_api_key
|
||||
# TRANSCRIPTION_MODEL=gpt-4o-transcribe-diarize
|
||||
|
||||
# ASR model (for local WhisperX profiles)
|
||||
ASR_MODEL=large-v3
|
||||
|
||||
# HuggingFace token (required for diarization with pyannote)
|
||||
# Get yours at: https://huggingface.co/settings/tokens
|
||||
# Must accept: https://huggingface.co/pyannote/speaker-diarization-3.1
|
||||
HF_TOKEN=
|
||||
|
||||
# =============================================================================
|
||||
# ASR PROXY — CLOUD PROFILE ONLY
|
||||
# =============================================================================
|
||||
# GCP project for GPU instances
|
||||
# GCP_PROJECT=your-gcp-project
|
||||
|
||||
# Monthly GPU budget limit in hours (default: 50)
|
||||
# MONTHLY_LIMIT_HOURS=50
|
||||
|
||||
# Idle timeout before auto-stopping GPU (seconds, default: 300)
|
||||
# IDLE_TIMEOUT=300
|
||||
|
||||
# =============================================================================
|
||||
# APPLICATION SETTINGS
|
||||
# =============================================================================
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_EMAIL=admin@example.com
|
||||
ADMIN_PASSWORD=changeme
|
||||
|
||||
ALLOW_REGISTRATION=false
|
||||
TIMEZONE="America/Toronto"
|
||||
LOG_LEVEL=ERROR
|
||||
LOCALE=fr_CA
|
||||
DEFAULT_LANGUAGE=fr
|
||||
SHOW_USERNAMES_IN_UI=true
|
||||
SESSION_COOKIE_HTTPONLY=true
|
||||
SESSION_COOKIE_SAMESITE=Lax
|
||||
SESSION_COOKIE_SECURE=true
|
||||
|
||||
# =============================================================================
|
||||
# OPTIONAL FEATURES
|
||||
# =============================================================================
|
||||
ENABLE_INQUIRE_MODE=false
|
||||
ENABLE_AUTO_PROCESSING=false
|
||||
ENABLE_AUTO_EXPORT=false
|
||||
ENABLE_AUTO_DELETION=false
|
||||
ENABLE_INTERNAL_SHARING=true
|
||||
ENABLE_PUBLIC_SHARING=true
|
||||
ENABLE_FOLDERS=true
|
||||
VIDEO_RETENTION=true
|
||||
USERS_CAN_DELETE=true
|
||||
|
||||
# =============================================================================
|
||||
# BACKGROUND PROCESSING
|
||||
# =============================================================================
|
||||
JOB_QUEUE_WORKERS=4
|
||||
SUMMARY_QUEUE_WORKERS=4
|
||||
JOB_MAX_RETRIES=3
|
||||
MAX_CONCURRENT_UPLOADS=3
|
||||
|
||||
# =============================================================================
|
||||
# TRANSCRIPTION SETTINGS
|
||||
# =============================================================================
|
||||
TRANSCRIPTION_CONNECTOR=asr_endpoint
|
||||
USE_NEW_TRANSCRIPTION_ARCHITECTURE=true
|
||||
ENABLE_CHUNKING=true
|
||||
CHUNK_LIMIT=2400s
|
||||
CHUNK_OVERLAP_SECONDS=5
|
||||
|
||||
# =============================================================================
|
||||
# LLM / SUMMARY SETTINGS
|
||||
# =============================================================================
|
||||
SUMMARY_LANGUAGE=fr
|
||||
SUMMARY_MAX_TOKENS=16000
|
||||
CHAT_MAX_TOKENS=12000
|
||||
ENABLE_STREAM_OPTIONS=false
|
||||
ENABLE_THINKING=false
|
||||
|
||||
# =============================================================================
|
||||
# DOCKER/DATABASE
|
||||
# =============================================================================
|
||||
SQLALCHEMY_DATABASE_URI=sqlite:////data/instance/transcriptions.db
|
||||
UPLOAD_FOLDER=/data/uploads
|
||||
40
deployment/docker/docker-compose.cloud.yml
Normal file
40
deployment/docker/docker-compose.cloud.yml
Normal file
@@ -0,0 +1,40 @@
|
||||
# DictIA — Cloud deployment (VPS + ASR Proxy GCP GPU)
|
||||
#
|
||||
# Usage:
|
||||
# docker compose -f deployment/docker/docker-compose.cloud.yml up -d
|
||||
#
|
||||
# ASR is handled by the external asr-proxy (port 9090) which auto-starts
|
||||
# a GCP GPU instance on demand. DictIA connects via host.docker.internal.
|
||||
|
||||
services:
|
||||
dictia:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: Dockerfile
|
||||
image: innova-ai/dictia:latest
|
||||
container_name: dictia
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8899:8899"
|
||||
env_file:
|
||||
- ../../.env
|
||||
environment:
|
||||
- LOG_LEVEL=${LOG_LEVEL:-ERROR}
|
||||
- ASR_BASE_URL=http://host.docker.internal:9090
|
||||
volumes:
|
||||
- ../../data/uploads:/data/uploads
|
||||
- ../../data/instance:/data/instance
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
healthcheck:
|
||||
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8899/health')"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
networks:
|
||||
- dictia-network
|
||||
|
||||
networks:
|
||||
dictia-network:
|
||||
driver: bridge
|
||||
64
deployment/docker/docker-compose.local-cpu.yml
Normal file
64
deployment/docker/docker-compose.local-cpu.yml
Normal file
@@ -0,0 +1,64 @@
|
||||
# DictIA — Local CPU deployment (WhisperX on CPU + DictIA)
|
||||
#
|
||||
# Usage:
|
||||
# docker compose -f deployment/docker/docker-compose.local-cpu.yml up -d
|
||||
#
|
||||
# Warning: CPU transcription is significantly slower than GPU.
|
||||
# Expect ~10x real-time for large files (e.g., 1h audio = ~10h processing).
|
||||
|
||||
services:
|
||||
whisperx-asr:
|
||||
image: ghcr.io/jim60105/whisperx-asr:latest
|
||||
container_name: whisperx-asr
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "9000:9000"
|
||||
environment:
|
||||
- ASR_MODEL=${ASR_MODEL:-large-v3}
|
||||
- ASR_ENGINE=whisperx
|
||||
- DEVICE=cpu
|
||||
- COMPUTE_TYPE=float32
|
||||
- HF_TOKEN=${HF_TOKEN:-}
|
||||
volumes:
|
||||
- whisperx-cache:/root/.cache
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 18G
|
||||
networks:
|
||||
- dictia-network
|
||||
|
||||
dictia:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: Dockerfile
|
||||
image: innova-ai/dictia:latest
|
||||
container_name: dictia
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8899:8899"
|
||||
env_file:
|
||||
- ../../.env
|
||||
environment:
|
||||
- LOG_LEVEL=${LOG_LEVEL:-ERROR}
|
||||
- ASR_BASE_URL=http://whisperx-asr:9000
|
||||
volumes:
|
||||
- ../../data/uploads:/data/uploads
|
||||
- ../../data/instance:/data/instance
|
||||
depends_on:
|
||||
- whisperx-asr
|
||||
healthcheck:
|
||||
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8899/health')"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
networks:
|
||||
- dictia-network
|
||||
|
||||
volumes:
|
||||
whisperx-cache:
|
||||
|
||||
networks:
|
||||
dictia-network:
|
||||
driver: bridge
|
||||
69
deployment/docker/docker-compose.local-gpu.yml
Normal file
69
deployment/docker/docker-compose.local-gpu.yml
Normal file
@@ -0,0 +1,69 @@
|
||||
# DictIA — Local GPU deployment (WhisperX on NVIDIA GPU + DictIA)
|
||||
#
|
||||
# Usage:
|
||||
# docker compose -f deployment/docker/docker-compose.local-gpu.yml up -d
|
||||
#
|
||||
# Prerequisites:
|
||||
# - NVIDIA GPU with CUDA support
|
||||
# - nvidia-container-toolkit installed
|
||||
# - Docker configured with nvidia runtime
|
||||
|
||||
services:
|
||||
whisperx-asr:
|
||||
image: ghcr.io/jim60105/whisperx-asr:latest-cuda
|
||||
container_name: whisperx-asr
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "9000:9000"
|
||||
environment:
|
||||
- ASR_MODEL=${ASR_MODEL:-large-v3}
|
||||
- ASR_ENGINE=whisperx
|
||||
- DEVICE=cuda
|
||||
- COMPUTE_TYPE=float16
|
||||
- HF_TOKEN=${HF_TOKEN:-}
|
||||
volumes:
|
||||
- whisperx-cache:/root/.cache
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: 1
|
||||
capabilities: [gpu]
|
||||
networks:
|
||||
- dictia-network
|
||||
|
||||
dictia:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: Dockerfile
|
||||
image: innova-ai/dictia:latest
|
||||
container_name: dictia
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8899:8899"
|
||||
env_file:
|
||||
- ../../.env
|
||||
environment:
|
||||
- LOG_LEVEL=${LOG_LEVEL:-ERROR}
|
||||
- ASR_BASE_URL=http://whisperx-asr:9000
|
||||
volumes:
|
||||
- ../../data/uploads:/data/uploads
|
||||
- ../../data/instance:/data/instance
|
||||
depends_on:
|
||||
- whisperx-asr
|
||||
healthcheck:
|
||||
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8899/health')"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
networks:
|
||||
- dictia-network
|
||||
|
||||
volumes:
|
||||
whisperx-cache:
|
||||
|
||||
networks:
|
||||
dictia-network:
|
||||
driver: bridge
|
||||
118
deployment/docs/LOCAL-SETUP.md
Normal file
118
deployment/docs/LOCAL-SETUP.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# Setup Local — DictIA
|
||||
|
||||
Guide pour deployer DictIA localement avec GPU NVIDIA ou CPU.
|
||||
|
||||
## Profil local-gpu
|
||||
|
||||
### Prerequis
|
||||
|
||||
- NVIDIA GPU avec support CUDA
|
||||
- [nvidia-container-toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html)
|
||||
- Docker + Docker Compose V2
|
||||
- 8GB+ RAM (16GB recommande)
|
||||
- Token HuggingFace (pour la diarisation)
|
||||
|
||||
### Installation nvidia-container-toolkit
|
||||
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | \
|
||||
sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
|
||||
curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
|
||||
sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
|
||||
sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y nvidia-container-toolkit
|
||||
sudo nvidia-ctk runtime configure --runtime=docker
|
||||
sudo systemctl restart docker
|
||||
|
||||
# Verifier
|
||||
docker run --rm --gpus all nvidia/cuda:12.0-base nvidia-smi
|
||||
```
|
||||
|
||||
### Setup DictIA
|
||||
|
||||
```bash
|
||||
cd dictia
|
||||
bash deployment/setup.sh --profile local-gpu
|
||||
```
|
||||
|
||||
Le setup va verifier:
|
||||
- nvidia-container-toolkit installe
|
||||
- GPU accessible depuis Docker
|
||||
- Assez de RAM disponible
|
||||
|
||||
### Configuration du modele
|
||||
|
||||
Par defaut, WhisperX utilise `large-v3`. Pour changer:
|
||||
|
||||
```bash
|
||||
# Editer .env
|
||||
ASR_MODEL=large-v3 # Meilleure qualite
|
||||
# ASR_MODEL=medium # Plus rapide, qualite correcte
|
||||
# ASR_MODEL=small # Tres rapide, qualite reduite
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Profil local-cpu
|
||||
|
||||
### Prerequis
|
||||
|
||||
- Docker + Docker Compose V2
|
||||
- 18GB+ RAM (WhisperX CPU est gourmand)
|
||||
- Patience (transcription ~10x temps reel)
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
cd dictia
|
||||
bash deployment/setup.sh --profile local-cpu
|
||||
```
|
||||
|
||||
### Limitations
|
||||
|
||||
- Transcription lente: 1h d'audio prend ~10h
|
||||
- Utilise float32 (pas de GPU acceleration)
|
||||
- Limite memoire a 18GB par defaut
|
||||
- Recommande pour: tests, petits fichiers, demos
|
||||
|
||||
Pour reduire l'utilisation memoire, utiliser un modele plus petit:
|
||||
|
||||
```bash
|
||||
# Editer .env
|
||||
ASR_MODEL=small # ou medium, base, tiny
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
# Health check
|
||||
bash deployment/tools/health-check.sh
|
||||
|
||||
# Test rapide: ouvrir le navigateur
|
||||
open http://localhost:8899
|
||||
|
||||
# Verifier WhisperX
|
||||
curl http://localhost:9000/health
|
||||
```
|
||||
|
||||
## Gestion des containers
|
||||
|
||||
```bash
|
||||
COMPOSE_FILE=deployment/docker/docker-compose.local-gpu.yml # ou local-cpu
|
||||
|
||||
# Logs
|
||||
docker compose -f $COMPOSE_FILE logs -f
|
||||
|
||||
# Redemarrer
|
||||
docker compose -f $COMPOSE_FILE restart
|
||||
|
||||
# Arreter
|
||||
docker compose -f $COMPOSE_FILE down
|
||||
|
||||
# Voir l'utilisation GPU
|
||||
nvidia-smi # (profil GPU seulement)
|
||||
```
|
||||
136
deployment/docs/MAINTENANCE.md
Normal file
136
deployment/docs/MAINTENANCE.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# Maintenance — DictIA
|
||||
|
||||
## Backup
|
||||
|
||||
```bash
|
||||
# Backup complet (data, .env, volumes, stats ASR)
|
||||
bash deployment/tools/backup.sh
|
||||
|
||||
# Backup dans un repertoire specifique
|
||||
bash deployment/tools/backup.sh /mnt/backups
|
||||
```
|
||||
|
||||
Les backups sont sauvegardes dans `backups/` avec rotation automatique (garde les 5 derniers).
|
||||
|
||||
Contenu d'un backup:
|
||||
- `data/` — uploads et base de donnees SQLite
|
||||
- `dot-env` — fichier de configuration
|
||||
- `asr-usage-stats.json` — stats d'utilisation GPU
|
||||
- `whisperx-cache.tar.gz` — cache modeles (si volume Docker)
|
||||
- `manifest.json` — metadonnees du backup
|
||||
|
||||
### Schedule recommande
|
||||
|
||||
| Frequence | Action |
|
||||
|-----------|--------|
|
||||
| Quotidien | `bash deployment/tools/backup.sh` |
|
||||
| Hebdomadaire | Copier le backup sur un stockage externe |
|
||||
| Mensuel | Verifier la restauration sur un environnement de test |
|
||||
|
||||
Pour automatiser avec cron:
|
||||
|
||||
```bash
|
||||
# Backup quotidien a 3h du matin
|
||||
0 3 * * * /opt/dictia/deployment/tools/backup.sh >> /var/log/dictia-backup.log 2>&1
|
||||
```
|
||||
|
||||
## Restore
|
||||
|
||||
```bash
|
||||
# Lister les backups disponibles
|
||||
ls -la backups/
|
||||
|
||||
# Restaurer un backup
|
||||
bash deployment/tools/restore.sh backups/dictia-20260211-030000.tar.gz
|
||||
```
|
||||
|
||||
Le script:
|
||||
1. Valide l'archive (presence du manifest)
|
||||
2. Demande confirmation
|
||||
3. Arrete les containers
|
||||
4. Restaure les fichiers
|
||||
5. Redemarre les containers
|
||||
|
||||
## Mise a jour
|
||||
|
||||
```bash
|
||||
# Mise a jour complete (git pull + rebuild + restart)
|
||||
bash deployment/tools/update.sh
|
||||
|
||||
# Rebuild seulement (sans git pull)
|
||||
bash deployment/tools/update.sh --no-pull
|
||||
|
||||
# Git pull seulement (sans rebuild)
|
||||
bash deployment/tools/update.sh --no-build
|
||||
```
|
||||
|
||||
Le script:
|
||||
1. Detecte le profil actif automatiquement
|
||||
2. `git pull origin dictia-branding`
|
||||
3. `docker build -t innova-ai/dictia:latest .`
|
||||
4. Pull WhisperX upstream (profils locaux)
|
||||
5. `docker compose down && up -d`
|
||||
6. Attend le health check
|
||||
7. Nettoie les images dangling
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Health check
|
||||
|
||||
```bash
|
||||
# Diagnostic complet (humain)
|
||||
bash deployment/tools/health-check.sh
|
||||
|
||||
# JSON (pour alertes/scripts)
|
||||
bash deployment/tools/health-check.sh --json
|
||||
|
||||
# Code de sortie seulement (0=ok, 1=probleme)
|
||||
bash deployment/tools/health-check.sh --quiet
|
||||
```
|
||||
|
||||
### Logs
|
||||
|
||||
```bash
|
||||
# DictIA
|
||||
docker logs dictia -f --tail 100
|
||||
|
||||
# WhisperX (profils locaux)
|
||||
docker logs whisperx-asr -f --tail 100
|
||||
|
||||
# ASR Proxy (profil cloud)
|
||||
journalctl -u asr-proxy -f
|
||||
```
|
||||
|
||||
### Dashboard GPU (profil cloud)
|
||||
|
||||
Le dashboard de monitoring GPU est accessible a:
|
||||
- `http://localhost:9090` (local)
|
||||
- `https://votre-hostname.tailnet.ts.net:9443` (Tailscale)
|
||||
|
||||
Affiche: statut GPU, cout mensuel, historique des requetes, zones de fallback.
|
||||
|
||||
### Metriques cles
|
||||
|
||||
```bash
|
||||
# Espace disque (les transcriptions grossissent)
|
||||
df -h /opt/dictia/data/
|
||||
|
||||
# Utilisation memoire (WhisperX est gourmand)
|
||||
docker stats --no-stream
|
||||
|
||||
# Stats GPU (profil cloud)
|
||||
curl -s http://localhost:9090/stats | python3 -m json.tool
|
||||
```
|
||||
|
||||
## Maintenance Docker
|
||||
|
||||
```bash
|
||||
# Nettoyer les images orphelines
|
||||
docker image prune -f
|
||||
|
||||
# Nettoyer tout (attention: supprime les volumes non utilises)
|
||||
# docker system prune -a --volumes
|
||||
|
||||
# Verifier l'espace Docker
|
||||
docker system df
|
||||
```
|
||||
90
deployment/docs/QUICKSTART.md
Normal file
90
deployment/docs/QUICKSTART.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# Quickstart — DictIA
|
||||
|
||||
## Prerequis communs
|
||||
|
||||
- Docker + Docker Compose V2
|
||||
- Git
|
||||
- 2GB+ RAM disponible
|
||||
|
||||
```bash
|
||||
git clone https://gitea.innova-ai.ca/Innova-AI/dictia.git
|
||||
cd dictia
|
||||
git checkout dictia-branding
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Profil Cloud (VPS + GCP GPU)
|
||||
|
||||
Le GPU demarre automatiquement quand quelqu'un transcrit, et s'arrete apres 5 min d'inactivite.
|
||||
|
||||
```bash
|
||||
# 1. Setup interactif
|
||||
bash deployment/setup.sh --profile cloud
|
||||
|
||||
# 2. Setup ASR Proxy (GCP credentials requises)
|
||||
bash deployment/asr-proxy/setup.sh
|
||||
|
||||
# 3. Optionnel: Tailscale Serve pour HTTPS
|
||||
bash deployment/config/tailscale/setup-serve.sh
|
||||
```
|
||||
|
||||
**Requis**: credentials GCP (service account ou OAuth) dans `deployment/asr-proxy/gcp-credentials.json`.
|
||||
|
||||
---
|
||||
|
||||
## Profil Local GPU
|
||||
|
||||
Transcription locale sur GPU NVIDIA. Le plus rapide.
|
||||
|
||||
```bash
|
||||
# Prerequis: nvidia-container-toolkit
|
||||
# https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html
|
||||
|
||||
# Setup
|
||||
bash deployment/setup.sh --profile local-gpu
|
||||
```
|
||||
|
||||
**Requis**: token HuggingFace pour la diarisation (pyannote).
|
||||
|
||||
---
|
||||
|
||||
## Profil Local CPU
|
||||
|
||||
Transcription sur CPU. Lent mais fonctionnel pour tester.
|
||||
|
||||
```bash
|
||||
bash deployment/setup.sh --profile local-cpu
|
||||
```
|
||||
|
||||
Prevoir ~10x le temps reel (1h audio = ~10h de traitement).
|
||||
|
||||
---
|
||||
|
||||
## Apres l'installation
|
||||
|
||||
```bash
|
||||
# Verifier que tout fonctionne
|
||||
bash deployment/tools/health-check.sh
|
||||
|
||||
# Ouvrir DictIA
|
||||
open http://localhost:8899
|
||||
```
|
||||
|
||||
Se connecter avec les identifiants admin configures pendant le setup.
|
||||
|
||||
## Commandes utiles
|
||||
|
||||
```bash
|
||||
# Logs en temps reel
|
||||
docker compose -f deployment/docker/docker-compose.<profil>.yml logs -f
|
||||
|
||||
# Redemarrer
|
||||
docker compose -f deployment/docker/docker-compose.<profil>.yml restart
|
||||
|
||||
# Mise a jour
|
||||
bash deployment/tools/update.sh
|
||||
|
||||
# Backup
|
||||
bash deployment/tools/backup.sh
|
||||
```
|
||||
177
deployment/docs/TROUBLESHOOTING.md
Normal file
177
deployment/docs/TROUBLESHOOTING.md
Normal file
@@ -0,0 +1,177 @@
|
||||
# Troubleshooting — DictIA
|
||||
|
||||
## WhisperX OOM (Out of Memory)
|
||||
|
||||
**Symptome**: Container `whisperx-asr` crash ou restart en boucle.
|
||||
|
||||
**Cause**: Modele trop gros pour la RAM/VRAM disponible.
|
||||
|
||||
**Solutions**:
|
||||
```bash
|
||||
# Utiliser un modele plus petit dans .env
|
||||
ASR_MODEL=medium # au lieu de large-v3
|
||||
|
||||
# Augmenter la limite memoire (local-cpu)
|
||||
# Editer docker-compose.local-cpu.yml
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 24G # au lieu de 18G
|
||||
```
|
||||
|
||||
## Diarisation 403 Forbidden
|
||||
|
||||
**Symptome**: Erreur 403 lors de la transcription avec diarisation.
|
||||
|
||||
**Cause**: Token HuggingFace manquant ou conditions non acceptees.
|
||||
|
||||
**Solution**:
|
||||
1. Creer un token: https://huggingface.co/settings/tokens
|
||||
2. Accepter les conditions: https://huggingface.co/pyannote/speaker-diarization-3.1
|
||||
3. Ajouter dans `.env`:
|
||||
```bash
|
||||
HF_TOKEN=hf_votre_token
|
||||
```
|
||||
4. Redemarrer: `docker compose -f deployment/docker/docker-compose.<profil>.yml restart`
|
||||
|
||||
## GPU non detecte (local-gpu)
|
||||
|
||||
**Symptome**: `nvidia-smi` fonctionne mais Docker ne voit pas le GPU.
|
||||
|
||||
**Solution**:
|
||||
```bash
|
||||
# Installer nvidia-container-toolkit
|
||||
sudo apt install -y nvidia-container-toolkit
|
||||
sudo nvidia-ctk runtime configure --runtime=docker
|
||||
sudo systemctl restart docker
|
||||
|
||||
# Verifier
|
||||
docker run --rm --gpus all nvidia/cuda:12.0-base nvidia-smi
|
||||
```
|
||||
|
||||
## Upload echoue (fichiers volumineux)
|
||||
|
||||
**Symptome**: Upload de gros fichiers (>100MB) echoue.
|
||||
|
||||
**Causes possibles**:
|
||||
- Timeout Nginx/reverse proxy
|
||||
- Limite upload trop basse
|
||||
|
||||
**Solutions**:
|
||||
```bash
|
||||
# Si Nginx: verifier client_max_body_size dans dictia.conf
|
||||
client_max_body_size 500M;
|
||||
|
||||
# Si Tailscale Serve: pas de limite cote Tailscale
|
||||
|
||||
# Timeout gunicorn (dans le Dockerfile, deja a 600s)
|
||||
# Pour des fichiers tres longs, augmenter dans docker-compose:
|
||||
environment:
|
||||
- GUNICORN_TIMEOUT=1200
|
||||
```
|
||||
|
||||
## Container dictia "unhealthy"
|
||||
|
||||
**Symptome**: `docker ps` montre "unhealthy" pour le container dictia.
|
||||
|
||||
**Diagnostic**:
|
||||
```bash
|
||||
# Voir les logs
|
||||
docker logs dictia --tail 50
|
||||
|
||||
# Tester manuellement
|
||||
docker exec dictia python3 -c "import urllib.request; urllib.request.urlopen('http://localhost:8899/health')"
|
||||
```
|
||||
|
||||
**Causes courantes**:
|
||||
- `.env` mal configure (SECRET_KEY manquant)
|
||||
- Base de donnees corrompue (restaurer backup)
|
||||
- Port 8899 deja utilise
|
||||
|
||||
## ASR Proxy: "No GPU available"
|
||||
|
||||
**Symptome**: Transcription echoue avec "No GPU available in any Canadian zone".
|
||||
|
||||
**Causes**:
|
||||
- GCP n'a pas de GPU disponible (capacite epuisee)
|
||||
- Credentials GCP expirees
|
||||
- Budget mensuel atteint
|
||||
|
||||
**Diagnostic**:
|
||||
```bash
|
||||
# Verifier le statut du proxy
|
||||
curl -s http://localhost:9090/health | python3 -m json.tool
|
||||
|
||||
# Verifier les stats (budget)
|
||||
curl -s http://localhost:9090/stats | python3 -m json.tool
|
||||
|
||||
# Voir les logs
|
||||
journalctl -u asr-proxy --since "1 hour ago"
|
||||
```
|
||||
|
||||
**Solutions**:
|
||||
- Attendre (GCP libere des GPUs regulierement)
|
||||
- Le proxy reessaie automatiquement apres un cooldown de 3 minutes
|
||||
- Verifier le dashboard: http://localhost:9090
|
||||
|
||||
## Build Docker lent/echoue
|
||||
|
||||
**Symptome**: `docker build` prend trop de temps ou echoue.
|
||||
|
||||
**Solutions**:
|
||||
```bash
|
||||
# Limiter les ressources si le VPS est petit
|
||||
docker build --memory=2g --cpus=2 -t innova-ai/dictia:latest .
|
||||
|
||||
# Nettoyer le cache Docker si le disque est plein
|
||||
docker builder prune -f
|
||||
docker image prune -f
|
||||
```
|
||||
|
||||
## Base de donnees corrompue
|
||||
|
||||
**Symptome**: Erreur SQLite au demarrage.
|
||||
|
||||
**Solution**:
|
||||
```bash
|
||||
# Restaurer le dernier backup
|
||||
bash deployment/tools/restore.sh backups/dictia-LATEST.tar.gz
|
||||
|
||||
# Ou recreer la base (perd les donnees)
|
||||
rm data/instance/transcriptions.db
|
||||
docker compose -f deployment/docker/docker-compose.<profil>.yml restart
|
||||
```
|
||||
|
||||
## Port 8899 deja utilise
|
||||
|
||||
```bash
|
||||
# Trouver qui utilise le port
|
||||
sudo lsof -i :8899
|
||||
# ou
|
||||
sudo ss -tlnp | grep 8899
|
||||
|
||||
# Arreter le processus ou changer le port dans docker-compose
|
||||
ports:
|
||||
- "8900:8899" # utiliser 8900 a la place
|
||||
```
|
||||
|
||||
## Mise a jour qui casse tout
|
||||
|
||||
```bash
|
||||
# Rollback: revenir au commit precedent
|
||||
cd dictia
|
||||
git log --oneline -5 # trouver le bon commit
|
||||
git checkout <commit-hash>
|
||||
|
||||
# Rebuild et redemarrer
|
||||
docker build -t innova-ai/dictia:latest .
|
||||
docker compose -f deployment/docker/docker-compose.<profil>.yml down
|
||||
docker compose -f deployment/docker/docker-compose.<profil>.yml up -d
|
||||
```
|
||||
|
||||
## Commande de diagnostic rapide
|
||||
|
||||
```bash
|
||||
# Tout verifier d'un coup
|
||||
bash deployment/tools/health-check.sh --json | python3 -m json.tool
|
||||
```
|
||||
148
deployment/docs/VPS-SETUP.md
Normal file
148
deployment/docs/VPS-SETUP.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# Setup VPS from scratch — DictIA
|
||||
|
||||
Guide complet pour deployer DictIA sur un VPS Ubuntu.
|
||||
Teste sur OVH VPS avec Ubuntu 22.04/24.04.
|
||||
|
||||
## 1. Preparation du VPS
|
||||
|
||||
```bash
|
||||
# Mise a jour systeme
|
||||
sudo apt update && sudo apt upgrade -y
|
||||
|
||||
# Installer les essentiels
|
||||
sudo apt install -y curl git
|
||||
```
|
||||
|
||||
## 2. Docker
|
||||
|
||||
```bash
|
||||
# Installer Docker (methode officielle)
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
|
||||
# Ajouter l'utilisateur au groupe docker
|
||||
sudo usermod -aG docker $USER
|
||||
|
||||
# Se reconnecter pour appliquer le groupe
|
||||
exit
|
||||
# (reconnecter via SSH)
|
||||
|
||||
# Verifier
|
||||
docker --version
|
||||
docker compose version
|
||||
```
|
||||
|
||||
## 3. Tailscale (recommande)
|
||||
|
||||
Tailscale fournit un VPN mesh pour acceder au VPS sans exposer de ports publics.
|
||||
|
||||
```bash
|
||||
# Installer Tailscale
|
||||
curl -fsSL https://tailscale.com/install.sh | sh
|
||||
|
||||
# Connecter au tailnet
|
||||
sudo tailscale up
|
||||
|
||||
# Verifier
|
||||
tailscale status
|
||||
```
|
||||
|
||||
## 4. DictIA
|
||||
|
||||
```bash
|
||||
# Cloner le repo
|
||||
cd ~
|
||||
git clone https://gitea.innova-ai.ca/Innova-AI/dictia.git
|
||||
cd dictia
|
||||
git checkout dictia-branding
|
||||
|
||||
# Lancer le setup
|
||||
bash deployment/setup.sh --profile cloud
|
||||
```
|
||||
|
||||
Le setup va:
|
||||
- Generer le `.env` avec vos identifiants
|
||||
- Creer les repertoires de donnees
|
||||
- Builder l'image Docker
|
||||
- Demarrer les containers
|
||||
|
||||
## 5. ASR Proxy (GCP GPU)
|
||||
|
||||
```bash
|
||||
# Installer le proxy
|
||||
bash deployment/asr-proxy/setup.sh
|
||||
|
||||
# Ajouter les credentials GCP
|
||||
# Copier votre fichier de credentials dans:
|
||||
cp ~/gcp-credentials.json deployment/asr-proxy/gcp-credentials.json
|
||||
|
||||
# Demarrer le service
|
||||
sudo systemctl start asr-proxy
|
||||
sudo systemctl status asr-proxy
|
||||
```
|
||||
|
||||
## 6. Securite
|
||||
|
||||
```bash
|
||||
# Docker daemon config (log rotation)
|
||||
sudo cp deployment/security/docker-daemon.json /etc/docker/daemon.json
|
||||
sudo systemctl restart docker
|
||||
|
||||
# Firewall iptables (bloque trafic non-Tailscale)
|
||||
sudo bash deployment/security/iptables-rules.sh
|
||||
|
||||
# Service systemd pour les regles au boot
|
||||
sudo cp deployment/security/docker-iptables.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable docker-iptables
|
||||
```
|
||||
|
||||
## 7. Tailscale Serve (HTTPS)
|
||||
|
||||
```bash
|
||||
# Expose DictIA et le dashboard ASR via Tailscale HTTPS
|
||||
bash deployment/config/tailscale/setup-serve.sh
|
||||
|
||||
# Verifier
|
||||
tailscale serve status
|
||||
```
|
||||
|
||||
DictIA sera accessible a `https://votre-hostname.tailnet.ts.net/`.
|
||||
|
||||
## 8. Service systemd (auto-start)
|
||||
|
||||
```bash
|
||||
# Adapter le chemin dans le fichier si necessaire
|
||||
sudo cp deployment/config/systemd/dictia.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable dictia
|
||||
```
|
||||
|
||||
## 9. Verification
|
||||
|
||||
```bash
|
||||
# Health check complet
|
||||
bash deployment/tools/health-check.sh
|
||||
|
||||
# Verifier les endpoints
|
||||
curl -s http://localhost:8899/health
|
||||
curl -s http://localhost:9090/health
|
||||
```
|
||||
|
||||
## 10. Premier backup
|
||||
|
||||
```bash
|
||||
bash deployment/tools/backup.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checklist post-installation
|
||||
|
||||
- [ ] DictIA repond sur :8899
|
||||
- [ ] ASR Proxy repond sur :9090
|
||||
- [ ] Tailscale Serve configure
|
||||
- [ ] iptables: seul Tailscale peut acceder
|
||||
- [ ] Docker: log rotation configuree
|
||||
- [ ] Service systemd enable (auto-start au boot)
|
||||
- [ ] Premier backup effectue
|
||||
- [ ] Identifiants admin testes
|
||||
101
deployment/profiles/docker-compose.dictia16.yml
Normal file
101
deployment/profiles/docker-compose.dictia16.yml
Normal file
@@ -0,0 +1,101 @@
|
||||
# =============================================================================
|
||||
# DictIA 16 — Docker Compose
|
||||
# GPU : RTX 5070 Ti (16 Go VRAM)
|
||||
# =============================================================================
|
||||
#
|
||||
# Services :
|
||||
# - dictia : Application principale DictIA
|
||||
# - whisperx-asr : Service de transcription WhisperX Large-v3
|
||||
# - ollama : LLM local Mistral 7B (résumés, chat, Q&A)
|
||||
#
|
||||
# Démarrage :
|
||||
# 1. cp config/env.dictia16.example .env
|
||||
# 2. docker compose -f config/docker-compose.dictia16.yml up -d
|
||||
# 3. Télécharger Mistral : docker exec ollama ollama pull mistral
|
||||
#
|
||||
# Note : Aucune clé API nécessaire — tout tourne en local (100% privé).
|
||||
# =============================================================================
|
||||
|
||||
services:
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Application DictIA
|
||||
# ---------------------------------------------------------------------------
|
||||
dictia:
|
||||
image: dictia:latest
|
||||
container_name: dictia
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8899:8899"
|
||||
env_file:
|
||||
- ../.env
|
||||
environment:
|
||||
- LOG_LEVEL=ERROR
|
||||
volumes:
|
||||
- ../uploads:/data/uploads
|
||||
- ../instance:/data/instance
|
||||
# Décommenter pour l'export automatique :
|
||||
# - ../exports:/data/exports
|
||||
# Décommenter pour le traitement automatique :
|
||||
# - ../auto-process:/data/auto-process
|
||||
depends_on:
|
||||
- whisperx-asr
|
||||
- ollama
|
||||
networks:
|
||||
- dictia-net
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# WhisperX ASR — Transcription locale (WhisperX Large-v3)
|
||||
# RTX 5070 Ti : BATCH_SIZE=32, COMPUTE_TYPE=float16
|
||||
# ---------------------------------------------------------------------------
|
||||
whisperx-asr:
|
||||
image: murtazanasir/whisperx-asr-service:latest
|
||||
container_name: whisperx-asr
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- HF_TOKEN=${HF_TOKEN}
|
||||
- DEVICE=cuda
|
||||
- COMPUTE_TYPE=float16
|
||||
- BATCH_SIZE=32
|
||||
- DEFAULT_MODEL=large-v3
|
||||
volumes:
|
||||
- whisperx-models:/root/.cache
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: 1
|
||||
capabilities: [gpu]
|
||||
networks:
|
||||
- dictia-net
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ollama — LLM local Mistral 7B
|
||||
# Résumés, points d'action, Q&A — 100% local, aucune donnée externe
|
||||
# ---------------------------------------------------------------------------
|
||||
ollama:
|
||||
image: ollama/ollama:latest
|
||||
container_name: ollama
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ollama-models:/root/.ollama
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: 1
|
||||
capabilities: [gpu]
|
||||
networks:
|
||||
- dictia-net
|
||||
|
||||
networks:
|
||||
dictia-net:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
whisperx-models:
|
||||
driver: local
|
||||
ollama-models:
|
||||
driver: local
|
||||
75
deployment/profiles/docker-compose.dictia8.yml
Normal file
75
deployment/profiles/docker-compose.dictia8.yml
Normal file
@@ -0,0 +1,75 @@
|
||||
# =============================================================================
|
||||
# DictIA 8 — Docker Compose
|
||||
# GPU : RTX 5060 (8 Go VRAM)
|
||||
# =============================================================================
|
||||
#
|
||||
# Services :
|
||||
# - dictia : Application principale DictIA
|
||||
# - whisperx-asr : Service de transcription WhisperX Large-v3
|
||||
#
|
||||
# Démarrage :
|
||||
# 1. cp config/env.dictia8.example .env
|
||||
# 2. Remplir TEXT_MODEL_API_KEY dans .env
|
||||
# 3. docker compose -f config/docker-compose.dictia8.yml up -d
|
||||
# =============================================================================
|
||||
|
||||
services:
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Application DictIA
|
||||
# ---------------------------------------------------------------------------
|
||||
dictia:
|
||||
image: dictia:latest
|
||||
container_name: dictia
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8899:8899"
|
||||
env_file:
|
||||
- ../.env
|
||||
environment:
|
||||
- LOG_LEVEL=ERROR
|
||||
volumes:
|
||||
- ../uploads:/data/uploads
|
||||
- ../instance:/data/instance
|
||||
# Décommenter pour l'export automatique :
|
||||
# - ../exports:/data/exports
|
||||
# Décommenter pour le traitement automatique :
|
||||
# - ../auto-process:/data/auto-process
|
||||
depends_on:
|
||||
- whisperx-asr
|
||||
networks:
|
||||
- dictia-net
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# WhisperX ASR — Transcription locale (WhisperX Large-v3)
|
||||
# RTX 5060 : BATCH_SIZE=16, COMPUTE_TYPE=float16
|
||||
# ---------------------------------------------------------------------------
|
||||
whisperx-asr:
|
||||
image: murtazanasir/whisperx-asr-service:latest
|
||||
container_name: whisperx-asr
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- HF_TOKEN=${HF_TOKEN}
|
||||
- DEVICE=cuda
|
||||
- COMPUTE_TYPE=float16
|
||||
- BATCH_SIZE=16
|
||||
- DEFAULT_MODEL=large-v3
|
||||
volumes:
|
||||
- whisperx-models:/root/.cache
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
devices:
|
||||
- driver: nvidia
|
||||
count: 1
|
||||
capabilities: [gpu]
|
||||
networks:
|
||||
- dictia-net
|
||||
|
||||
networks:
|
||||
dictia-net:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
whisperx-models:
|
||||
driver: local
|
||||
134
deployment/profiles/env.dictia16.example
Normal file
134
deployment/profiles/env.dictia16.example
Normal file
@@ -0,0 +1,134 @@
|
||||
# =============================================================================
|
||||
# DictIA 16 — Configuration (.env)
|
||||
# GPU : RTX 5070 Ti (16 Go VRAM)
|
||||
# =============================================================================
|
||||
#
|
||||
# Architecture :
|
||||
# - Transcription : WhisperX Large-v3 (local, ~5,5 Go VRAM)
|
||||
# - LLM (résumés) : Mistral 7B local via Ollama (~6,4 Go VRAM)
|
||||
# - Mode : Séquentiel (transcription puis résumé)
|
||||
# - Total VRAM : ~11,9 Go / 16 Go (marge ~4,1 Go)
|
||||
#
|
||||
# Démarrage rapide :
|
||||
# 1. cp config/env.dictia16.example .env
|
||||
# 2. Aucune clé API nécessaire — tout tourne en local
|
||||
# 3. docker compose -f config/docker-compose.dictia16.yml up -d
|
||||
# =============================================================================
|
||||
|
||||
# =============================================================================
|
||||
# MODÈLE DE TEXTE — Résumés, titres, chat (LLM LOCAL)
|
||||
# =============================================================================
|
||||
# DictIA 16 utilise Mistral 7B en local via Ollama.
|
||||
# Aucune donnée ne quitte le serveur — 100% privé.
|
||||
|
||||
TEXT_MODEL_BASE_URL=http://ollama:11434/v1
|
||||
TEXT_MODEL_API_KEY=not-required
|
||||
TEXT_MODEL_NAME=mistral
|
||||
|
||||
# --- Modèle de chat séparé (optionnel) ---
|
||||
# Même modèle par défaut, mais peut être changé pour un modèle plus rapide.
|
||||
# CHAT_MODEL_API_KEY=not-required
|
||||
# CHAT_MODEL_BASE_URL=http://ollama:11434/v1
|
||||
# CHAT_MODEL_NAME=mistral
|
||||
|
||||
# =============================================================================
|
||||
# TRANSCRIPTION — WhisperX ASR local (REQUIS)
|
||||
# =============================================================================
|
||||
# WhisperX tourne en local dans un conteneur Docker séparé.
|
||||
# Le service ASR est défini dans docker-compose.dictia16.yml.
|
||||
|
||||
ASR_BASE_URL=http://whisperx-asr:9000
|
||||
|
||||
# Diarisation (identification automatique des locuteurs) — recommandé
|
||||
ASR_DIARIZE=true
|
||||
ASR_RETURN_SPEAKER_EMBEDDINGS=true
|
||||
|
||||
# Nombre de locuteurs attendus (optionnel — aide la précision)
|
||||
# ASR_MIN_SPEAKERS=1
|
||||
# ASR_MAX_SPEAKERS=6
|
||||
|
||||
# =============================================================================
|
||||
# PARAMÈTRES ADMINISTRATEUR
|
||||
# =============================================================================
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_EMAIL=admin@votreentreprise.com
|
||||
ADMIN_PASSWORD=changeme
|
||||
|
||||
# =============================================================================
|
||||
# ACCÈS ET INSCRIPTION
|
||||
# =============================================================================
|
||||
# Désactiver l'inscription publique (accès sur invitation uniquement)
|
||||
ALLOW_REGISTRATION=false
|
||||
|
||||
# Restreindre l'inscription aux domaines autorisés
|
||||
# Exemple : REGISTRATION_ALLOWED_DOMAINS=votreentreprise.com
|
||||
REGISTRATION_ALLOWED_DOMAINS=
|
||||
|
||||
# =============================================================================
|
||||
# FUSEAU HORAIRE
|
||||
# =============================================================================
|
||||
# Exemples : America/Toronto, America/Montreal, America/New_York, UTC
|
||||
TIMEZONE="America/Toronto"
|
||||
|
||||
# =============================================================================
|
||||
# LIMITES DE TOKENS
|
||||
# =============================================================================
|
||||
SUMMARY_MAX_TOKENS=8000
|
||||
CHAT_MAX_TOKENS=5000
|
||||
|
||||
# =============================================================================
|
||||
# COMPRESSION AUDIO
|
||||
# =============================================================================
|
||||
AUDIO_COMPRESS_UPLOADS=true
|
||||
AUDIO_CODEC=mp3
|
||||
AUDIO_BITRATE=128k
|
||||
|
||||
# =============================================================================
|
||||
# FONCTIONNALITÉS OPTIONNELLES
|
||||
# =============================================================================
|
||||
|
||||
# Inquire Mode — recherche IA sur tous les enregistrements
|
||||
# Peut être activé sur DictIA 16 (plus de VRAM disponible)
|
||||
ENABLE_INQUIRE_MODE=false
|
||||
|
||||
# Traitement automatique de fichiers (dossier surveillé)
|
||||
ENABLE_AUTO_PROCESSING=false
|
||||
# AUTO_PROCESS_MODE=admin_only
|
||||
# AUTO_PROCESS_WATCH_DIR=/data/auto-process
|
||||
|
||||
# Export automatique
|
||||
ENABLE_AUTO_EXPORT=false
|
||||
# AUTO_EXPORT_DIR=/data/exports
|
||||
# AUTO_EXPORT_TRANSCRIPTION=true
|
||||
# AUTO_EXPORT_SUMMARY=true
|
||||
|
||||
# Suppression automatique / rétention
|
||||
ENABLE_AUTO_DELETION=false
|
||||
# GLOBAL_RETENTION_DAYS=90
|
||||
# DELETION_MODE=audio_only
|
||||
|
||||
# =============================================================================
|
||||
# PARTAGE
|
||||
# =============================================================================
|
||||
ENABLE_INTERNAL_SHARING=false
|
||||
ENABLE_PUBLIC_SHARING=true
|
||||
USERS_CAN_DELETE=true
|
||||
|
||||
# =============================================================================
|
||||
# FILES D'ATTENTE DE TRAITEMENT
|
||||
# =============================================================================
|
||||
JOB_QUEUE_WORKERS=2
|
||||
SUMMARY_QUEUE_WORKERS=2
|
||||
JOB_MAX_RETRIES=3
|
||||
|
||||
# =============================================================================
|
||||
# BASE DE DONNÉES ET STOCKAGE
|
||||
# =============================================================================
|
||||
SQLALCHEMY_DATABASE_URI=sqlite:////data/instance/transcriptions.db
|
||||
UPLOAD_FOLDER=/data/uploads
|
||||
|
||||
# =============================================================================
|
||||
# JOURNALISATION
|
||||
# =============================================================================
|
||||
# ERROR = production (minimal), INFO = débogage, DEBUG = développement
|
||||
LOG_LEVEL=ERROR
|
||||
126
deployment/profiles/env.dictia8.example
Normal file
126
deployment/profiles/env.dictia8.example
Normal file
@@ -0,0 +1,126 @@
|
||||
# =============================================================================
|
||||
# DictIA 8 — Configuration (.env)
|
||||
# GPU : RTX 5060 (8 Go VRAM)
|
||||
# =============================================================================
|
||||
#
|
||||
# Architecture :
|
||||
# - Transcription : WhisperX Large-v3 (local, ~5,5 Go VRAM)
|
||||
# - LLM (résumés) : API cloud via OpenRouter (VRAM insuffisante pour LLM local)
|
||||
#
|
||||
# Démarrage rapide :
|
||||
# 1. cp config/env.dictia8.example .env
|
||||
# 2. Remplir TRANSCRIPTION_API_KEY et TEXT_MODEL_API_KEY
|
||||
# 3. docker compose -f config/docker-compose.dictia8.yml up -d
|
||||
# =============================================================================
|
||||
|
||||
# =============================================================================
|
||||
# MODÈLE DE TEXTE — Résumés, titres, chat (REQUIS)
|
||||
# =============================================================================
|
||||
# DictIA 8 utilise un LLM cloud via OpenRouter (VRAM insuffisante pour LLM local).
|
||||
# Inscrivez-vous sur https://openrouter.ai pour obtenir une clé API.
|
||||
|
||||
TEXT_MODEL_BASE_URL=https://openrouter.ai/api/v1
|
||||
TEXT_MODEL_API_KEY=votre_cle_openrouter
|
||||
TEXT_MODEL_NAME=openai/gpt-4o-mini
|
||||
|
||||
# =============================================================================
|
||||
# TRANSCRIPTION — WhisperX ASR local (REQUIS)
|
||||
# =============================================================================
|
||||
# WhisperX tourne en local dans un conteneur Docker séparé.
|
||||
# Le service ASR est défini dans docker-compose.dictia8.yml.
|
||||
|
||||
ASR_BASE_URL=http://whisperx-asr:9000
|
||||
|
||||
# Diarisation (identification automatique des locuteurs) — recommandé
|
||||
ASR_DIARIZE=true
|
||||
ASR_RETURN_SPEAKER_EMBEDDINGS=true
|
||||
|
||||
# Nombre de locuteurs attendus (optionnel — aide la précision)
|
||||
# ASR_MIN_SPEAKERS=1
|
||||
# ASR_MAX_SPEAKERS=6
|
||||
|
||||
# =============================================================================
|
||||
# PARAMÈTRES ADMINISTRATEUR
|
||||
# =============================================================================
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_EMAIL=admin@votreentreprise.com
|
||||
ADMIN_PASSWORD=changeme
|
||||
|
||||
# =============================================================================
|
||||
# ACCÈS ET INSCRIPTION
|
||||
# =============================================================================
|
||||
# Désactiver l'inscription publique (accès sur invitation uniquement)
|
||||
ALLOW_REGISTRATION=false
|
||||
|
||||
# Restreindre l'inscription aux domaines autorisés
|
||||
# Exemple : REGISTRATION_ALLOWED_DOMAINS=votreentreprise.com
|
||||
REGISTRATION_ALLOWED_DOMAINS=
|
||||
|
||||
# =============================================================================
|
||||
# FUSEAU HORAIRE
|
||||
# =============================================================================
|
||||
# Exemples : America/Toronto, America/Montreal, America/New_York, UTC
|
||||
TIMEZONE="America/Toronto"
|
||||
|
||||
# =============================================================================
|
||||
# LIMITES DE TOKENS
|
||||
# =============================================================================
|
||||
SUMMARY_MAX_TOKENS=8000
|
||||
CHAT_MAX_TOKENS=5000
|
||||
|
||||
# =============================================================================
|
||||
# COMPRESSION AUDIO
|
||||
# =============================================================================
|
||||
AUDIO_COMPRESS_UPLOADS=true
|
||||
AUDIO_CODEC=mp3
|
||||
AUDIO_BITRATE=128k
|
||||
|
||||
# =============================================================================
|
||||
# FONCTIONNALITÉS OPTIONNELLES
|
||||
# =============================================================================
|
||||
|
||||
# Inquire Mode — recherche IA sur tous les enregistrements
|
||||
# Désactivé sur DictIA 8 (VRAM insuffisante pour embeddings locaux)
|
||||
ENABLE_INQUIRE_MODE=false
|
||||
|
||||
# Traitement automatique de fichiers (dossier surveillé)
|
||||
ENABLE_AUTO_PROCESSING=false
|
||||
# AUTO_PROCESS_MODE=admin_only
|
||||
# AUTO_PROCESS_WATCH_DIR=/data/auto-process
|
||||
|
||||
# Export automatique
|
||||
ENABLE_AUTO_EXPORT=false
|
||||
# AUTO_EXPORT_DIR=/data/exports
|
||||
# AUTO_EXPORT_TRANSCRIPTION=true
|
||||
# AUTO_EXPORT_SUMMARY=true
|
||||
|
||||
# Suppression automatique / rétention
|
||||
ENABLE_AUTO_DELETION=false
|
||||
# GLOBAL_RETENTION_DAYS=90
|
||||
# DELETION_MODE=audio_only
|
||||
|
||||
# =============================================================================
|
||||
# PARTAGE
|
||||
# =============================================================================
|
||||
ENABLE_INTERNAL_SHARING=false
|
||||
ENABLE_PUBLIC_SHARING=true
|
||||
USERS_CAN_DELETE=true
|
||||
|
||||
# =============================================================================
|
||||
# FILES D'ATTENTE DE TRAITEMENT
|
||||
# =============================================================================
|
||||
JOB_QUEUE_WORKERS=2
|
||||
SUMMARY_QUEUE_WORKERS=2
|
||||
JOB_MAX_RETRIES=3
|
||||
|
||||
# =============================================================================
|
||||
# BASE DE DONNÉES ET STOCKAGE
|
||||
# =============================================================================
|
||||
SQLALCHEMY_DATABASE_URI=sqlite:////data/instance/transcriptions.db
|
||||
UPLOAD_FOLDER=/data/uploads
|
||||
|
||||
# =============================================================================
|
||||
# JOURNALISATION
|
||||
# =============================================================================
|
||||
# ERROR = production (minimal), INFO = débogage, DEBUG = développement
|
||||
LOG_LEVEL=ERROR
|
||||
8
deployment/security/docker-daemon.json
Normal file
8
deployment/security/docker-daemon.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"log-driver": "json-file",
|
||||
"log-opts": {
|
||||
"max-size": "10m",
|
||||
"max-file": "3"
|
||||
},
|
||||
"storage-driver": "overlay2"
|
||||
}
|
||||
12
deployment/security/docker-iptables.service
Normal file
12
deployment/security/docker-iptables.service
Normal file
@@ -0,0 +1,12 @@
|
||||
[Unit]
|
||||
Description=DictIA Docker iptables rules
|
||||
After=docker.service tailscaled.service
|
||||
Requires=docker.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
ExecStart=/bin/bash /opt/dictia/deployment/security/iptables-rules.sh
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
36
deployment/security/iptables-rules.sh
Normal file
36
deployment/security/iptables-rules.sh
Normal file
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
# DictIA — iptables rules for cloud VPS
|
||||
#
|
||||
# Allows Docker internal traffic to reach the ASR proxy on port 9090.
|
||||
# Blocks direct external access to Docker container IPs.
|
||||
# Tailscale + UFW handle the main firewall — this script adds Docker-specific rules.
|
||||
#
|
||||
# Usage: sudo bash iptables-rules.sh
|
||||
set -euo pipefail
|
||||
|
||||
echo "=== DictIA iptables rules ==="
|
||||
|
||||
# Allow Docker containers (172.16.0.0/12) to reach ASR proxy on port 9090
|
||||
# This rule goes BEFORE the default DROP policy so containers can talk to the proxy
|
||||
iptables -C INPUT -s 172.16.0.0/12 -p tcp --dport 9090 -j ACCEPT 2>/dev/null \
|
||||
|| iptables -I INPUT 1 -s 172.16.0.0/12 -p tcp --dport 9090 -j ACCEPT
|
||||
|
||||
# Block direct external access to Docker container IPs (raw table, before conntrack)
|
||||
# Protects containers on non-default bridge networks (e.g., dictia-network)
|
||||
for NETWORK_ID in $(docker network ls --filter driver=bridge --format '{{.ID}}' 2>/dev/null); do
|
||||
BRIDGE=$(docker network inspect "$NETWORK_ID" --format '{{.Options.com.docker.network.bridge.name}}' 2>/dev/null || echo "")
|
||||
[ -z "$BRIDGE" ] && continue
|
||||
[ "$BRIDGE" = "docker0" ] && continue
|
||||
|
||||
for CONTAINER_IP in $(docker network inspect "$NETWORK_ID" \
|
||||
--format '{{range .Containers}}{{.IPv4Address}} {{end}}' 2>/dev/null); do
|
||||
IP="${CONTAINER_IP%/*}"
|
||||
[ -z "$IP" ] && continue
|
||||
iptables -t raw -C PREROUTING -d "$IP" ! -i "$BRIDGE" -j DROP 2>/dev/null \
|
||||
|| iptables -t raw -A PREROUTING -d "$IP" ! -i "$BRIDGE" -j DROP
|
||||
echo " Protected $IP on $BRIDGE"
|
||||
done
|
||||
done
|
||||
|
||||
echo "Rules applied. Tailscale + Docker internal traffic allowed."
|
||||
echo "Verify with: sudo iptables -L -n -t raw"
|
||||
300
deployment/setup.sh
Executable file
300
deployment/setup.sh
Executable file
@@ -0,0 +1,300 @@
|
||||
#!/usr/bin/env bash
|
||||
# DictIA — Main setup script
|
||||
#
|
||||
# Interactive installer that detects hardware and configures the appropriate
|
||||
# deployment profile (cloud, local-cpu, local-gpu).
|
||||
#
|
||||
# Usage:
|
||||
# bash deployment/setup.sh # Interactive mode
|
||||
# bash deployment/setup.sh --profile cloud # Non-interactive
|
||||
# bash deployment/setup.sh --profile local-gpu
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
PROFILE=""
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--profile=*) PROFILE="${arg#*=}" ;;
|
||||
--profile) shift_next=true ;;
|
||||
*)
|
||||
if [ "${shift_next:-false}" = true ]; then
|
||||
PROFILE="$arg"
|
||||
shift_next=false
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# --- Colors ---
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${CYAN}[INFO]${NC} $*"; }
|
||||
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
err() { echo -e "${RED}[ERROR]${NC} $*"; }
|
||||
|
||||
echo
|
||||
echo -e "${CYAN}========================================${NC}"
|
||||
echo -e "${CYAN} DictIA — Setup${NC}"
|
||||
echo -e "${CYAN}========================================${NC}"
|
||||
echo
|
||||
|
||||
# ==========================================================================
|
||||
# 1. Hardware Detection
|
||||
# ==========================================================================
|
||||
info "Detecting hardware..."
|
||||
|
||||
# Docker
|
||||
if command -v docker &>/dev/null && docker info &>/dev/null; then
|
||||
DOCKER_VERSION=$(docker --version | grep -oP '\d+\.\d+\.\d+' | head -1)
|
||||
ok "Docker $DOCKER_VERSION"
|
||||
else
|
||||
err "Docker not found or not running."
|
||||
echo " Install Docker: https://docs.docker.com/engine/install/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Docker Compose
|
||||
if docker compose version &>/dev/null; then
|
||||
COMPOSE_VERSION=$(docker compose version --short 2>/dev/null || echo "unknown")
|
||||
ok "Docker Compose $COMPOSE_VERSION"
|
||||
else
|
||||
err "Docker Compose not found."
|
||||
echo " Docker Compose V2 is required (comes with Docker Desktop or docker-compose-plugin)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# GPU
|
||||
HAS_GPU=false
|
||||
if command -v nvidia-smi &>/dev/null; then
|
||||
GPU_NAME=$(nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null | head -1 || echo "")
|
||||
if [ -n "$GPU_NAME" ]; then
|
||||
HAS_GPU=true
|
||||
ok "NVIDIA GPU: $GPU_NAME"
|
||||
# Check nvidia-container-toolkit
|
||||
if docker info 2>/dev/null | grep -qi nvidia; then
|
||||
ok "nvidia-container-toolkit detected"
|
||||
else
|
||||
warn "nvidia-container-toolkit not detected. Required for local-gpu profile."
|
||||
echo " Install: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
info "No NVIDIA GPU detected"
|
||||
fi
|
||||
|
||||
# RAM
|
||||
if command -v free &>/dev/null; then
|
||||
RAM_GB=$(free -g | awk '/Mem:/{print $2}')
|
||||
info "RAM: ${RAM_GB}GB"
|
||||
fi
|
||||
|
||||
# Disk
|
||||
DISK_AVAIL=$(df -h "$PROJECT_DIR" 2>/dev/null | awk 'NR==2{print $4}')
|
||||
info "Disk available: $DISK_AVAIL"
|
||||
|
||||
echo
|
||||
|
||||
# ==========================================================================
|
||||
# 2. Profile Selection
|
||||
# ==========================================================================
|
||||
if [ -z "$PROFILE" ]; then
|
||||
echo -e "${CYAN}Select deployment profile:${NC}"
|
||||
echo
|
||||
echo " 1) cloud — VPS with ASR Proxy (GCP GPU on demand)"
|
||||
echo " Best for: remote servers, pay-per-use GPU"
|
||||
echo
|
||||
echo " 2) local-gpu — Local NVIDIA GPU for transcription"
|
||||
echo " Best for: dedicated GPU server, fastest"
|
||||
if [ "$HAS_GPU" = false ]; then
|
||||
echo -e " ${YELLOW}(No GPU detected on this machine)${NC}"
|
||||
fi
|
||||
echo
|
||||
echo " 3) local-cpu — CPU-only transcription (slow)"
|
||||
echo " Best for: testing, low-volume usage"
|
||||
echo
|
||||
read -rp "Choice [1-3]: " CHOICE
|
||||
case "$CHOICE" in
|
||||
1) PROFILE="cloud" ;;
|
||||
2) PROFILE="local-gpu" ;;
|
||||
3) PROFILE="local-cpu" ;;
|
||||
*) err "Invalid choice"; exit 1 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
COMPOSE_FILE="$SCRIPT_DIR/docker/docker-compose.$PROFILE.yml"
|
||||
if [ ! -f "$COMPOSE_FILE" ]; then
|
||||
err "Compose file not found: $COMPOSE_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ok "Profile: $PROFILE"
|
||||
echo
|
||||
|
||||
# ==========================================================================
|
||||
# 3. Generate .env
|
||||
# ==========================================================================
|
||||
ENV_FILE="$PROJECT_DIR/.env"
|
||||
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
warn ".env already exists. Keeping existing configuration."
|
||||
echo " To reconfigure, delete .env and re-run setup."
|
||||
else
|
||||
info "Generating .env..."
|
||||
|
||||
# Generate secret key
|
||||
SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))" 2>/dev/null \
|
||||
|| openssl rand -hex 32 2>/dev/null \
|
||||
|| head -c 64 /dev/urandom | xxd -p | head -c 64)
|
||||
|
||||
# Prompt for admin credentials
|
||||
read -rp "Admin username [admin]: " ADMIN_USER
|
||||
ADMIN_USER="${ADMIN_USER:-admin}"
|
||||
read -rp "Admin email [admin@example.com]: " ADMIN_EMAIL
|
||||
ADMIN_EMAIL="${ADMIN_EMAIL:-admin@example.com}"
|
||||
read -rsp "Admin password: " ADMIN_PASS
|
||||
echo
|
||||
ADMIN_PASS="${ADMIN_PASS:-changeme}"
|
||||
|
||||
# Prompt for text model API key
|
||||
echo
|
||||
info "DictIA needs a text/LLM API key for summaries, titles, and chat."
|
||||
echo " Recommended: OpenRouter (https://openrouter.ai) — access to many models"
|
||||
read -rp "Text model API key (or press Enter to skip): " TEXT_API_KEY
|
||||
TEXT_API_KEY="${TEXT_API_KEY:-your_openrouter_api_key}"
|
||||
|
||||
# HuggingFace token for diarization
|
||||
if [ "$PROFILE" != "cloud" ]; then
|
||||
echo
|
||||
info "For speaker diarization, a HuggingFace token is needed."
|
||||
echo " Get one at: https://huggingface.co/settings/tokens"
|
||||
echo " Accept model: https://huggingface.co/pyannote/speaker-diarization-3.1"
|
||||
read -rp "HuggingFace token (or press Enter to skip): " HF_TOKEN
|
||||
HF_TOKEN="${HF_TOKEN:-}"
|
||||
else
|
||||
HF_TOKEN=""
|
||||
fi
|
||||
|
||||
# Write .env
|
||||
cp "$SCRIPT_DIR/docker/.env.example" "$ENV_FILE"
|
||||
sed -i "s|SECRET_KEY=.*|SECRET_KEY=$SECRET_KEY|" "$ENV_FILE"
|
||||
sed -i "s|DICTIA_PROFILE=.*|DICTIA_PROFILE=$PROFILE|" "$ENV_FILE"
|
||||
sed -i "s|ADMIN_USERNAME=.*|ADMIN_USERNAME=$ADMIN_USER|" "$ENV_FILE"
|
||||
sed -i "s|ADMIN_EMAIL=.*|ADMIN_EMAIL=$ADMIN_EMAIL|" "$ENV_FILE"
|
||||
sed -i "s|ADMIN_PASSWORD=.*|ADMIN_PASSWORD=$ADMIN_PASS|" "$ENV_FILE"
|
||||
sed -i "s|TEXT_MODEL_API_KEY=.*|TEXT_MODEL_API_KEY=$TEXT_API_KEY|" "$ENV_FILE"
|
||||
sed -i "s|HF_TOKEN=.*|HF_TOKEN=$HF_TOKEN|" "$ENV_FILE"
|
||||
|
||||
ok ".env generated"
|
||||
fi
|
||||
echo
|
||||
|
||||
# ==========================================================================
|
||||
# 4. Create data directories
|
||||
# ==========================================================================
|
||||
info "Creating data directories..."
|
||||
mkdir -p "$PROJECT_DIR/data/uploads" "$PROJECT_DIR/data/instance"
|
||||
ok "data/uploads and data/instance created"
|
||||
echo
|
||||
|
||||
# ==========================================================================
|
||||
# 5. Profile-specific setup
|
||||
# ==========================================================================
|
||||
case "$PROFILE" in
|
||||
cloud)
|
||||
info "Cloud profile — setting up ASR Proxy..."
|
||||
if [ -f "$SCRIPT_DIR/asr-proxy/setup.sh" ]; then
|
||||
echo " Run the ASR proxy setup separately:"
|
||||
echo " bash $SCRIPT_DIR/asr-proxy/setup.sh"
|
||||
fi
|
||||
echo
|
||||
info "Setting up iptables rules..."
|
||||
if [ -f "$SCRIPT_DIR/security/iptables-rules.sh" ] && [ "$(id -u)" -eq 0 ]; then
|
||||
bash "$SCRIPT_DIR/security/iptables-rules.sh"
|
||||
else
|
||||
echo " Run as root: sudo bash $SCRIPT_DIR/security/iptables-rules.sh"
|
||||
fi
|
||||
echo
|
||||
info "Setting up Tailscale Serve..."
|
||||
if command -v tailscale &>/dev/null; then
|
||||
echo " Run: bash $SCRIPT_DIR/config/tailscale/setup-serve.sh"
|
||||
else
|
||||
warn "Tailscale not installed."
|
||||
echo " Install: curl -fsSL https://tailscale.com/install.sh | sh"
|
||||
fi
|
||||
;;
|
||||
local-gpu)
|
||||
info "Local GPU profile — verifying NVIDIA runtime..."
|
||||
if docker info 2>/dev/null | grep -qi nvidia; then
|
||||
ok "NVIDIA Docker runtime available"
|
||||
# Quick GPU test
|
||||
if docker run --rm --gpus all nvidia/cuda:12.0-base nvidia-smi &>/dev/null; then
|
||||
ok "GPU test passed"
|
||||
else
|
||||
warn "GPU test failed. Check nvidia-container-toolkit installation."
|
||||
fi
|
||||
else
|
||||
err "NVIDIA Docker runtime not found."
|
||||
echo " Install nvidia-container-toolkit and restart Docker."
|
||||
echo " https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html"
|
||||
fi
|
||||
;;
|
||||
local-cpu)
|
||||
warn "CPU-only transcription is significantly slower than GPU."
|
||||
echo " Expect ~10x real-time (1h audio = ~10h processing)."
|
||||
echo " Consider local-gpu or cloud profile for better performance."
|
||||
;;
|
||||
esac
|
||||
|
||||
echo
|
||||
|
||||
# ==========================================================================
|
||||
# 6. Build and start
|
||||
# ==========================================================================
|
||||
info "Building DictIA Docker image..."
|
||||
cd "$PROJECT_DIR"
|
||||
docker build -t innova-ai/dictia:latest .
|
||||
ok "Image built"
|
||||
|
||||
echo
|
||||
info "Starting DictIA ($PROFILE profile)..."
|
||||
docker compose -f "$COMPOSE_FILE" up -d
|
||||
ok "Containers started"
|
||||
|
||||
# ==========================================================================
|
||||
# 7. Health check
|
||||
# ==========================================================================
|
||||
echo
|
||||
info "Waiting for DictIA to become healthy..."
|
||||
RETRIES=30
|
||||
for i in $(seq 1 $RETRIES); do
|
||||
if curl -sf -o /dev/null -m 5 http://localhost:8899/health 2>/dev/null; then
|
||||
ok "DictIA is healthy!"
|
||||
break
|
||||
fi
|
||||
if [ "$i" -eq "$RETRIES" ]; then
|
||||
warn "Health check timeout. Check logs: docker compose -f $COMPOSE_FILE logs"
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
|
||||
echo
|
||||
echo -e "${GREEN}========================================${NC}"
|
||||
echo -e "${GREEN} DictIA is ready!${NC}"
|
||||
echo -e "${GREEN}========================================${NC}"
|
||||
echo
|
||||
echo " App: http://localhost:8899"
|
||||
echo " Profile: $PROFILE"
|
||||
echo " Compose: $COMPOSE_FILE"
|
||||
echo
|
||||
echo " Tools:"
|
||||
echo " Update: bash deployment/tools/update.sh"
|
||||
echo " Backup: bash deployment/tools/backup.sh"
|
||||
echo " Health check: bash deployment/tools/health-check.sh"
|
||||
echo
|
||||
89
deployment/tools/backup.sh
Normal file
89
deployment/tools/backup.sh
Normal file
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env bash
|
||||
# DictIA — Backup script
|
||||
#
|
||||
# Creates a timestamped backup of data, env, and Docker volumes.
|
||||
# Keeps the last N backups (default: 5).
|
||||
#
|
||||
# Usage: bash backup.sh [BACKUP_DIR]
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
BACKUP_BASE="${1:-$PROJECT_DIR/backups}"
|
||||
KEEP_COUNT=5
|
||||
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
||||
BACKUP_DIR="$BACKUP_BASE/dictia-$TIMESTAMP"
|
||||
|
||||
echo "=== DictIA Backup ==="
|
||||
echo "Project: $PROJECT_DIR"
|
||||
echo "Backup: $BACKUP_DIR"
|
||||
echo
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
# 1. Data directory
|
||||
if [ -d "$PROJECT_DIR/data" ]; then
|
||||
echo "[1/4] Backing up data/..."
|
||||
cp -a "$PROJECT_DIR/data" "$BACKUP_DIR/data"
|
||||
else
|
||||
echo "[1/4] No data/ directory found, skipping."
|
||||
fi
|
||||
|
||||
# 2. Environment file
|
||||
if [ -f "$PROJECT_DIR/.env" ]; then
|
||||
echo "[2/4] Backing up .env..."
|
||||
cp "$PROJECT_DIR/.env" "$BACKUP_DIR/dot-env"
|
||||
else
|
||||
echo "[2/4] No .env found, skipping."
|
||||
fi
|
||||
|
||||
# 3. ASR Proxy stats
|
||||
ASR_STATS="$PROJECT_DIR/deployment/asr-proxy/usage-stats.json"
|
||||
if [ -f "$ASR_STATS" ]; then
|
||||
echo "[3/4] Backing up ASR proxy stats..."
|
||||
cp "$ASR_STATS" "$BACKUP_DIR/asr-usage-stats.json"
|
||||
else
|
||||
echo "[3/4] No ASR proxy stats, skipping."
|
||||
fi
|
||||
|
||||
# 4. Docker volumes (if using managed volumes)
|
||||
echo "[4/4] Checking Docker volumes..."
|
||||
if docker volume ls --format '{{.Name}}' 2>/dev/null | grep -q "whisperx-cache"; then
|
||||
echo " Exporting whisperx-cache volume..."
|
||||
docker run --rm -v whisperx-cache:/source -v "$BACKUP_DIR":/backup \
|
||||
alpine tar czf /backup/whisperx-cache.tar.gz -C /source . 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Write manifest
|
||||
cat > "$BACKUP_DIR/manifest.json" <<MANIFEST
|
||||
{
|
||||
"timestamp": "$TIMESTAMP",
|
||||
"project_dir": "$PROJECT_DIR",
|
||||
"hostname": "$(hostname)",
|
||||
"contents": {
|
||||
"data": $([ -d "$BACKUP_DIR/data" ] && echo "true" || echo "false"),
|
||||
"env": $([ -f "$BACKUP_DIR/dot-env" ] && echo "true" || echo "false"),
|
||||
"asr_stats": $([ -f "$BACKUP_DIR/asr-usage-stats.json" ] && echo "true" || echo "false"),
|
||||
"whisperx_cache": $([ -f "$BACKUP_DIR/whisperx-cache.tar.gz" ] && echo "true" || echo "false")
|
||||
}
|
||||
}
|
||||
MANIFEST
|
||||
|
||||
# Compress
|
||||
echo
|
||||
echo "Compressing backup..."
|
||||
ARCHIVE="$BACKUP_BASE/dictia-$TIMESTAMP.tar.gz"
|
||||
tar czf "$ARCHIVE" -C "$BACKUP_BASE" "dictia-$TIMESTAMP"
|
||||
rm -rf "$BACKUP_DIR"
|
||||
echo "Archive: $ARCHIVE ($(du -h "$ARCHIVE" | cut -f1))"
|
||||
|
||||
# Rotate old backups
|
||||
BACKUP_COUNT=$(ls -1 "$BACKUP_BASE"/dictia-*.tar.gz 2>/dev/null | wc -l)
|
||||
if [ "$BACKUP_COUNT" -gt "$KEEP_COUNT" ]; then
|
||||
echo
|
||||
echo "Rotating backups (keeping last $KEEP_COUNT)..."
|
||||
ls -1t "$BACKUP_BASE"/dictia-*.tar.gz | tail -n +"$((KEEP_COUNT + 1))" | xargs rm -f
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "=== Backup complete ==="
|
||||
157
deployment/tools/health-check.sh
Normal file
157
deployment/tools/health-check.sh
Normal file
@@ -0,0 +1,157 @@
|
||||
#!/usr/bin/env bash
|
||||
# DictIA — Health check diagnostic
|
||||
#
|
||||
# Checks Docker, containers, endpoints, disk, RAM, and GPU.
|
||||
#
|
||||
# Usage:
|
||||
# bash health-check.sh # Human-readable output
|
||||
# bash health-check.sh --json # JSON output
|
||||
# bash health-check.sh --quiet # Exit code only (0=ok, 1=issue)
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
OUTPUT="human"
|
||||
ISSUES=0
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--json) OUTPUT="json" ;;
|
||||
--quiet) OUTPUT="quiet" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
declare -A CHECKS
|
||||
|
||||
check() {
|
||||
local name="$1"
|
||||
local status="$2"
|
||||
local detail="${3:-}"
|
||||
CHECKS["$name"]="$status|$detail"
|
||||
if [ "$status" = "error" ] || [ "$status" = "warning" ]; then
|
||||
ISSUES=$((ISSUES + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Docker ---
|
||||
if command -v docker &>/dev/null && docker info &>/dev/null; then
|
||||
check "docker" "ok" "Docker daemon running"
|
||||
else
|
||||
check "docker" "error" "Docker not available"
|
||||
fi
|
||||
|
||||
# --- Containers ---
|
||||
DICTIA_STATUS=$(docker inspect --format='{{.State.Health.Status}}' dictia 2>/dev/null || echo "not_found")
|
||||
if [ "$DICTIA_STATUS" = "healthy" ]; then
|
||||
check "container_dictia" "ok" "healthy"
|
||||
elif [ "$DICTIA_STATUS" = "not_found" ]; then
|
||||
check "container_dictia" "error" "container not found"
|
||||
else
|
||||
check "container_dictia" "warning" "$DICTIA_STATUS"
|
||||
fi
|
||||
|
||||
WHISPERX_STATUS=$(docker inspect --format='{{.State.Status}}' whisperx-asr 2>/dev/null || echo "not_found")
|
||||
if [ "$WHISPERX_STATUS" = "running" ]; then
|
||||
check "container_whisperx" "ok" "running"
|
||||
elif [ "$WHISPERX_STATUS" = "not_found" ]; then
|
||||
check "container_whisperx" "info" "not present (cloud profile?)"
|
||||
else
|
||||
check "container_whisperx" "warning" "$WHISPERX_STATUS"
|
||||
fi
|
||||
|
||||
# --- Endpoints ---
|
||||
if curl -sf -o /dev/null -m 5 http://localhost:8899/health 2>/dev/null; then
|
||||
check "endpoint_dictia" "ok" "http://localhost:8899 responding"
|
||||
else
|
||||
check "endpoint_dictia" "error" "http://localhost:8899 not responding"
|
||||
fi
|
||||
|
||||
if curl -sf -o /dev/null -m 5 http://localhost:9000/health 2>/dev/null; then
|
||||
check "endpoint_whisperx" "ok" "http://localhost:9000 responding"
|
||||
else
|
||||
check "endpoint_whisperx" "info" "http://localhost:9000 not responding"
|
||||
fi
|
||||
|
||||
if curl -sf -o /dev/null -m 5 http://localhost:9090/health 2>/dev/null; then
|
||||
check "endpoint_asr_proxy" "ok" "http://localhost:9090 responding"
|
||||
else
|
||||
check "endpoint_asr_proxy" "info" "http://localhost:9090 not responding"
|
||||
fi
|
||||
|
||||
# --- Disk ---
|
||||
DISK_USED=$(df -h "$PROJECT_DIR" 2>/dev/null | awk 'NR==2{print $5}' | tr -d '%')
|
||||
if [ -n "$DISK_USED" ]; then
|
||||
if [ "$DISK_USED" -gt 90 ]; then
|
||||
check "disk" "error" "${DISK_USED}% used"
|
||||
elif [ "$DISK_USED" -gt 80 ]; then
|
||||
check "disk" "warning" "${DISK_USED}% used"
|
||||
else
|
||||
check "disk" "ok" "${DISK_USED}% used"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- RAM ---
|
||||
if command -v free &>/dev/null; then
|
||||
MEM_TOTAL=$(free -m | awk '/Mem:/{print $2}')
|
||||
MEM_AVAIL=$(free -m | awk '/Mem:/{print $7}')
|
||||
MEM_USED_PCT=$(( (MEM_TOTAL - MEM_AVAIL) * 100 / MEM_TOTAL ))
|
||||
if [ "$MEM_USED_PCT" -gt 90 ]; then
|
||||
check "memory" "warning" "${MEM_USED_PCT}% used (${MEM_AVAIL}MB available)"
|
||||
else
|
||||
check "memory" "ok" "${MEM_USED_PCT}% used (${MEM_AVAIL}MB available)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- GPU ---
|
||||
if command -v nvidia-smi &>/dev/null; then
|
||||
GPU_INFO=$(nvidia-smi --query-gpu=name,memory.used,memory.total --format=csv,noheader 2>/dev/null || echo "error")
|
||||
if [ "$GPU_INFO" != "error" ]; then
|
||||
check "gpu" "ok" "$GPU_INFO"
|
||||
else
|
||||
check "gpu" "warning" "nvidia-smi present but query failed"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Output ---
|
||||
if [ "$OUTPUT" = "json" ]; then
|
||||
echo "{"
|
||||
echo " \"timestamp\": \"$(date -Is)\","
|
||||
echo " \"issues\": $ISSUES,"
|
||||
echo " \"checks\": {"
|
||||
FIRST=true
|
||||
for name in "${!CHECKS[@]}"; do
|
||||
IFS='|' read -r status detail <<< "${CHECKS[$name]}"
|
||||
if [ "$FIRST" = true ]; then
|
||||
FIRST=false
|
||||
else
|
||||
echo ","
|
||||
fi
|
||||
printf ' "%s": {"status": "%s", "detail": "%s"}' "$name" "$status" "$detail"
|
||||
done
|
||||
echo
|
||||
echo " }"
|
||||
echo "}"
|
||||
elif [ "$OUTPUT" = "quiet" ]; then
|
||||
exit $( [ "$ISSUES" -eq 0 ] && echo 0 || echo 1 )
|
||||
else
|
||||
echo "=== DictIA Health Check ==="
|
||||
echo
|
||||
for name in docker container_dictia container_whisperx endpoint_dictia endpoint_whisperx endpoint_asr_proxy disk memory gpu; do
|
||||
if [ -n "${CHECKS[$name]+x}" ]; then
|
||||
IFS='|' read -r status detail <<< "${CHECKS[$name]}"
|
||||
case "$status" in
|
||||
ok) ICON="[OK]" ;;
|
||||
warning) ICON="[!!]" ;;
|
||||
error) ICON="[ERR]" ;;
|
||||
info) ICON="[--]" ;;
|
||||
esac
|
||||
printf " %-22s %s %s\n" "$name" "$ICON" "$detail"
|
||||
fi
|
||||
done
|
||||
echo
|
||||
if [ "$ISSUES" -eq 0 ]; then
|
||||
echo "All checks passed."
|
||||
else
|
||||
echo "$ISSUES issue(s) found."
|
||||
fi
|
||||
fi
|
||||
101
deployment/tools/restore.sh
Normal file
101
deployment/tools/restore.sh
Normal file
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env bash
|
||||
# DictIA — Restore script
|
||||
#
|
||||
# Restores a DictIA backup archive created by backup.sh.
|
||||
#
|
||||
# Usage: bash restore.sh <ARCHIVE_PATH> [PROJECT_DIR]
|
||||
set -euo pipefail
|
||||
|
||||
ARCHIVE="${1:-}"
|
||||
PROJECT_DIR="${2:-$(cd "$(dirname "$0")/../.." && pwd)}"
|
||||
|
||||
if [ -z "$ARCHIVE" ] || [ ! -f "$ARCHIVE" ]; then
|
||||
echo "Usage: bash restore.sh <backup-archive.tar.gz> [project-dir]"
|
||||
echo
|
||||
echo "Available backups:"
|
||||
ls -1t "$PROJECT_DIR/backups"/dictia-*.tar.gz 2>/dev/null | head -5 || echo " (none found)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== DictIA Restore ==="
|
||||
echo "Archive: $ARCHIVE"
|
||||
echo "Target: $PROJECT_DIR"
|
||||
echo
|
||||
|
||||
# Validate archive
|
||||
echo "Validating archive..."
|
||||
TMPDIR=$(mktemp -d)
|
||||
tar xzf "$ARCHIVE" -C "$TMPDIR"
|
||||
BACKUP_DIR=$(ls -1d "$TMPDIR"/dictia-* | head -1)
|
||||
|
||||
if [ ! -f "$BACKUP_DIR/manifest.json" ]; then
|
||||
echo "ERROR: Invalid backup archive (no manifest.json)"
|
||||
rm -rf "$TMPDIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Manifest:"
|
||||
cat "$BACKUP_DIR/manifest.json"
|
||||
echo
|
||||
echo
|
||||
|
||||
# Confirmation
|
||||
read -rp "This will overwrite current data. Continue? [y/N] " CONFIRM
|
||||
if [ "$CONFIRM" != "y" ] && [ "$CONFIRM" != "Y" ]; then
|
||||
echo "Aborted."
|
||||
rm -rf "$TMPDIR"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Stop services
|
||||
echo
|
||||
echo "Stopping DictIA services..."
|
||||
COMPOSE_FILE=""
|
||||
for f in cloud local-cpu local-gpu; do
|
||||
if [ -f "$PROJECT_DIR/deployment/docker/docker-compose.$f.yml" ]; then
|
||||
COMPOSE_FILE="$PROJECT_DIR/deployment/docker/docker-compose.$f.yml"
|
||||
fi
|
||||
done
|
||||
if [ -n "$COMPOSE_FILE" ]; then
|
||||
docker compose -f "$COMPOSE_FILE" down 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Restore data
|
||||
if [ -d "$BACKUP_DIR/data" ]; then
|
||||
echo "Restoring data/..."
|
||||
rm -rf "$PROJECT_DIR/data"
|
||||
cp -a "$BACKUP_DIR/data" "$PROJECT_DIR/data"
|
||||
fi
|
||||
|
||||
# Restore .env
|
||||
if [ -f "$BACKUP_DIR/dot-env" ]; then
|
||||
echo "Restoring .env..."
|
||||
cp "$BACKUP_DIR/dot-env" "$PROJECT_DIR/.env"
|
||||
fi
|
||||
|
||||
# Restore ASR stats
|
||||
if [ -f "$BACKUP_DIR/asr-usage-stats.json" ]; then
|
||||
echo "Restoring ASR proxy stats..."
|
||||
cp "$BACKUP_DIR/asr-usage-stats.json" "$PROJECT_DIR/deployment/asr-proxy/usage-stats.json"
|
||||
fi
|
||||
|
||||
# Restore Docker volumes
|
||||
if [ -f "$BACKUP_DIR/whisperx-cache.tar.gz" ]; then
|
||||
echo "Restoring whisperx-cache volume..."
|
||||
docker volume create whisperx-cache 2>/dev/null || true
|
||||
docker run --rm -v whisperx-cache:/target -v "$BACKUP_DIR":/backup \
|
||||
alpine sh -c "cd /target && tar xzf /backup/whisperx-cache.tar.gz" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Cleanup
|
||||
rm -rf "$TMPDIR"
|
||||
|
||||
# Restart services
|
||||
echo
|
||||
echo "Restarting DictIA..."
|
||||
if [ -n "$COMPOSE_FILE" ]; then
|
||||
docker compose -f "$COMPOSE_FILE" up -d
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "=== Restore complete ==="
|
||||
105
deployment/tools/update.sh
Normal file
105
deployment/tools/update.sh
Normal file
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env bash
|
||||
# DictIA — Update script
|
||||
#
|
||||
# Pulls latest code, rebuilds Docker image, and restarts services.
|
||||
# Detects the active deployment profile automatically.
|
||||
#
|
||||
# Usage: bash update.sh [--no-pull] [--no-build]
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
NO_PULL=false
|
||||
NO_BUILD=false
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--no-pull) NO_PULL=true ;;
|
||||
--no-build) NO_BUILD=true ;;
|
||||
*) echo "Unknown option: $arg"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "=== DictIA Update ==="
|
||||
echo "Project: $PROJECT_DIR"
|
||||
echo
|
||||
|
||||
# 1. Detect active compose file
|
||||
COMPOSE_FILE=""
|
||||
PROFILE=""
|
||||
for f in cloud local-cpu local-gpu; do
|
||||
CF="$PROJECT_DIR/deployment/docker/docker-compose.$f.yml"
|
||||
if [ -f "$CF" ] && docker compose -f "$CF" ps --quiet 2>/dev/null | grep -q .; then
|
||||
COMPOSE_FILE="$CF"
|
||||
PROFILE="$f"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$COMPOSE_FILE" ]; then
|
||||
# Fallback: check .env for profile
|
||||
if [ -f "$PROJECT_DIR/.env" ]; then
|
||||
PROFILE=$(grep -E '^DICTIA_PROFILE=' "$PROJECT_DIR/.env" 2>/dev/null | cut -d= -f2 || echo "cloud")
|
||||
fi
|
||||
PROFILE="${PROFILE:-cloud}"
|
||||
COMPOSE_FILE="$PROJECT_DIR/deployment/docker/docker-compose.$PROFILE.yml"
|
||||
fi
|
||||
|
||||
echo "Profile: $PROFILE"
|
||||
echo "Compose: $COMPOSE_FILE"
|
||||
echo
|
||||
|
||||
# 2. Git pull
|
||||
if [ "$NO_PULL" = false ]; then
|
||||
echo "[1/5] Pulling latest code..."
|
||||
cd "$PROJECT_DIR"
|
||||
git pull origin dictia-branding
|
||||
else
|
||||
echo "[1/5] Skipping git pull (--no-pull)"
|
||||
fi
|
||||
|
||||
# 3. Rebuild DictIA image
|
||||
if [ "$NO_BUILD" = false ]; then
|
||||
echo "[2/5] Building DictIA image..."
|
||||
cd "$PROJECT_DIR"
|
||||
docker build -t innova-ai/dictia:latest .
|
||||
else
|
||||
echo "[2/5] Skipping build (--no-build)"
|
||||
fi
|
||||
|
||||
# 3b. Pull upstream images (WhisperX) if local profile
|
||||
if [ "$PROFILE" != "cloud" ] && [ "$NO_BUILD" = false ]; then
|
||||
echo "[3/5] Pulling upstream images (WhisperX)..."
|
||||
docker compose -f "$COMPOSE_FILE" pull whisperx-asr 2>/dev/null || true
|
||||
else
|
||||
echo "[3/5] Skipping upstream pull (cloud profile or --no-build)"
|
||||
fi
|
||||
|
||||
# 4. Restart containers
|
||||
echo "[4/5] Restarting containers..."
|
||||
docker compose -f "$COMPOSE_FILE" down
|
||||
docker compose -f "$COMPOSE_FILE" up -d
|
||||
|
||||
# 5. Wait for health
|
||||
echo "[5/5] Waiting for health check..."
|
||||
RETRIES=30
|
||||
for i in $(seq 1 $RETRIES); do
|
||||
if docker compose -f "$COMPOSE_FILE" ps | grep -q "healthy"; then
|
||||
echo " DictIA is healthy!"
|
||||
break
|
||||
fi
|
||||
if [ "$i" -eq "$RETRIES" ]; then
|
||||
echo " WARNING: Health check timeout. Check: docker compose -f $COMPOSE_FILE logs"
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
|
||||
# Cleanup dangling images
|
||||
echo
|
||||
echo "Cleaning up old images..."
|
||||
docker image prune -f 2>/dev/null || true
|
||||
|
||||
echo
|
||||
echo "=== Update complete ==="
|
||||
echo "DictIA: http://localhost:8899"
|
||||
docker compose -f "$COMPOSE_FILE" ps
|
||||
2
requirements-embeddings.txt
Normal file
2
requirements-embeddings.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
sentence-transformers==2.7.0
|
||||
huggingface-hub>=0.19.0
|
||||
25
requirements.txt
Normal file
25
requirements.txt
Normal file
@@ -0,0 +1,25 @@
|
||||
flask==2.3.3
|
||||
flask-sqlalchemy==3.1.1
|
||||
flask-login==0.6.3
|
||||
flask-wtf==1.2.2
|
||||
flask-bcrypt==1.0.1
|
||||
Flask-Limiter==3.5.0
|
||||
flask-openapi3>=3.0.0
|
||||
pydantic>=2.0.0
|
||||
authlib>=1.3.0
|
||||
itsdangerous>=2.1.0
|
||||
email-validator==2.2.0
|
||||
openai>=2.2.0
|
||||
pywebpush==1.14.0
|
||||
werkzeug==2.3.7
|
||||
gunicorn==21.2.0
|
||||
python-dotenv==1.0.0
|
||||
markdown==3.5.1
|
||||
pytz==2024.1
|
||||
Babel==2.12.1
|
||||
bleach==6.1.0
|
||||
python-docx==1.1.0
|
||||
numpy==1.24.3
|
||||
scikit-learn==1.3.0
|
||||
scipy<1.15
|
||||
psycopg2-binary>=2.9.0
|
||||
94
scripts/create_admin.py
Normal file
94
scripts/create_admin.py
Normal file
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
import getpass
|
||||
from email_validator import validate_email, EmailNotValidError
|
||||
|
||||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
# Try to import from app context
|
||||
try:
|
||||
from flask import current_app
|
||||
app = current_app._get_current_object()
|
||||
with app.app_context():
|
||||
db = app.extensions['sqlalchemy'].db
|
||||
User = app.extensions['sqlalchemy'].db.metadata.tables['user']
|
||||
bcrypt = app.extensions.get('bcrypt')
|
||||
except (RuntimeError, AttributeError, KeyError):
|
||||
# If not in app context, import directly
|
||||
try:
|
||||
from src.app import app, db, User, bcrypt
|
||||
except ImportError as e:
|
||||
print(f"Error: Could not import required modules: {e}")
|
||||
print("Make sure create_admin.py is runnable and PYTHONPATH is set.")
|
||||
sys.exit(1)
|
||||
|
||||
def create_admin_user():
|
||||
"""
|
||||
Create an admin user interactively.
|
||||
"""
|
||||
print("Creating admin user for Speakr application")
|
||||
print("=========================================")
|
||||
|
||||
# Get username
|
||||
while True:
|
||||
username = input("Enter username (min 3 characters): ").strip()
|
||||
if len(username) < 3:
|
||||
print("Username must be at least 3 characters long.")
|
||||
continue
|
||||
|
||||
# Check if username already exists
|
||||
with app.app_context():
|
||||
existing_user = db.session.query(User).filter_by(username=username).first()
|
||||
if existing_user:
|
||||
print(f"Username '{username}' already exists. Please choose another.")
|
||||
continue
|
||||
break
|
||||
|
||||
# Get email
|
||||
skip_domain_check = os.environ.get('SKIP_EMAIL_DOMAIN_CHECK', 'false').lower() == 'true'
|
||||
while True:
|
||||
email = input("Enter email address: ").strip()
|
||||
try:
|
||||
# Validate email (skip DNS/MX check if SKIP_EMAIL_DOMAIN_CHECK=true)
|
||||
validate_email(email, check_deliverability=not skip_domain_check)
|
||||
|
||||
# Check if email already exists
|
||||
with app.app_context():
|
||||
existing_email = db.session.query(User).filter_by(email=email).first()
|
||||
if existing_email:
|
||||
print(f"Email '{email}' already exists. Please use another.")
|
||||
continue
|
||||
break
|
||||
except EmailNotValidError as e:
|
||||
print(f"Invalid email: {str(e)}")
|
||||
|
||||
# Get password
|
||||
while True:
|
||||
password = getpass.getpass("Enter password (min 8 characters): ")
|
||||
if len(password) < 8:
|
||||
print("Password must be at least 8 characters long.")
|
||||
continue
|
||||
|
||||
confirm_password = getpass.getpass("Confirm password: ")
|
||||
if password != confirm_password:
|
||||
print("Passwords do not match. Please try again.")
|
||||
continue
|
||||
break
|
||||
|
||||
# Create user
|
||||
with app.app_context():
|
||||
hashed_password = bcrypt.generate_password_hash(password).decode('utf-8')
|
||||
new_user = User(username=username, email=email, password=hashed_password, is_admin=True)
|
||||
db.session.add(new_user)
|
||||
db.session.commit()
|
||||
|
||||
print("\nAdmin user created successfully!")
|
||||
print(f"Username: {username}")
|
||||
print(f"Email: {email}")
|
||||
print("You can now log in to the application with these credentials.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_admin_user()
|
||||
25
scripts/docker-entrypoint.sh
Normal file
25
scripts/docker-entrypoint.sh
Normal file
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Create necessary directories
|
||||
mkdir -p /data/uploads /data/instance
|
||||
chmod 755 /data/uploads /data/instance
|
||||
|
||||
# Initialize the database if it doesn't exist
|
||||
if [ ! -f /data/instance/transcriptions.db ]; then
|
||||
echo "Database doesn't exist. Creating new database..."
|
||||
python -c "from src.app import app, db; app.app_context().push(); db.create_all()"
|
||||
echo "Database created successfully."
|
||||
else
|
||||
echo "Database exists. Checking for schema updates..."
|
||||
python -c "from src.app import app; app.app_context().push()"
|
||||
fi
|
||||
|
||||
# Check if we need to create an admin user (regardless of whether the database exists)
|
||||
if [ -n "$ADMIN_USERNAME" ] && [ -n "$ADMIN_EMAIL" ] && [ -n "$ADMIN_PASSWORD" ]; then
|
||||
echo "Creating admin user using environment variables..."
|
||||
cd /app && python scripts/docker_create_admin.py
|
||||
fi
|
||||
|
||||
# Start the application
|
||||
exec "$@"
|
||||
92
scripts/docker_create_admin.py
Normal file
92
scripts/docker_create_admin.py
Normal file
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
from email_validator import validate_email, EmailNotValidError
|
||||
|
||||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
# Try to import from app context
|
||||
try:
|
||||
from flask import current_app
|
||||
app = current_app._get_current_object()
|
||||
with app.app_context():
|
||||
db = app.extensions['sqlalchemy'].db
|
||||
User = app.extensions['sqlalchemy'].db.metadata.tables['user']
|
||||
bcrypt = app.extensions.get('bcrypt')
|
||||
except (RuntimeError, AttributeError, KeyError):
|
||||
# If not in app context, import directly
|
||||
try:
|
||||
from src.app import app, db, User, bcrypt
|
||||
except ImportError as e:
|
||||
print(f"Error: Could not import required modules: {e}")
|
||||
print("Make sure docker_create_admin.py is runnable and PYTHONPATH is set.")
|
||||
sys.exit(1)
|
||||
|
||||
def create_admin_user_from_env():
|
||||
"""
|
||||
Create an admin user from environment variables.
|
||||
Required environment variables:
|
||||
- ADMIN_USERNAME
|
||||
- ADMIN_EMAIL
|
||||
- ADMIN_PASSWORD
|
||||
"""
|
||||
print("Creating admin user for Speakr application from environment variables")
|
||||
print("=================================================================")
|
||||
|
||||
# Get values from environment variables
|
||||
username = os.environ.get('ADMIN_USERNAME')
|
||||
email = os.environ.get('ADMIN_EMAIL')
|
||||
password = os.environ.get('ADMIN_PASSWORD')
|
||||
|
||||
# Validate required environment variables
|
||||
if not username or not email or not password:
|
||||
print("Error: ADMIN_USERNAME, ADMIN_EMAIL, and ADMIN_PASSWORD environment variables must be set.")
|
||||
sys.exit(1)
|
||||
|
||||
# Validate username
|
||||
if len(username) < 3:
|
||||
print("Error: Username must be at least 3 characters long.")
|
||||
sys.exit(1)
|
||||
|
||||
# Validate email (skip DNS/MX check if SKIP_EMAIL_DOMAIN_CHECK=true)
|
||||
skip_domain_check = os.environ.get('SKIP_EMAIL_DOMAIN_CHECK', 'false').lower() == 'true'
|
||||
try:
|
||||
validate_email(email, check_deliverability=not skip_domain_check)
|
||||
except EmailNotValidError as e:
|
||||
print(f"Error: Invalid email: {str(e)}")
|
||||
sys.exit(1)
|
||||
|
||||
# Validate password
|
||||
if len(password) < 8:
|
||||
print("Error: Password must be at least 8 characters long.")
|
||||
sys.exit(1)
|
||||
|
||||
# Create user
|
||||
with app.app_context():
|
||||
# Check if username already exists
|
||||
existing_user = db.session.query(User).filter_by(username=username).first()
|
||||
if existing_user:
|
||||
print(f"User with username '{username}' already exists.")
|
||||
sys.exit(0)
|
||||
|
||||
# Check if email already exists
|
||||
existing_email = db.session.query(User).filter_by(email=email).first()
|
||||
if existing_email:
|
||||
print(f"User with email '{email}' already exists.")
|
||||
sys.exit(0)
|
||||
|
||||
# Create new admin user
|
||||
hashed_password = bcrypt.generate_password_hash(password).decode('utf-8')
|
||||
new_user = User(username=username, email=email, password=hashed_password, is_admin=True)
|
||||
db.session.add(new_user)
|
||||
db.session.commit()
|
||||
|
||||
print("\nAdmin user created successfully!")
|
||||
print(f"Username: {username}")
|
||||
print(f"Email: {email}")
|
||||
print("You can now log in to the application with these credentials.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_admin_user_from_env()
|
||||
116
scripts/download_offline_deps.py
Normal file
116
scripts/download_offline_deps.py
Normal file
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Download all CDN dependencies for offline deployment
|
||||
"""
|
||||
|
||||
import os
|
||||
import requests
|
||||
from pathlib import Path
|
||||
|
||||
# Base directory for vendor files
|
||||
VENDOR_DIR = Path(__file__).parent.parent / "static" / "vendor"
|
||||
|
||||
# Dependencies to download
|
||||
DEPENDENCIES = {
|
||||
"css": {
|
||||
"fontawesome.min.css": "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css",
|
||||
"easymde.min.css": "https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.css",
|
||||
},
|
||||
"js": {
|
||||
"tailwind.min.js": "https://cdn.tailwindcss.com/3.4.0",
|
||||
"vue.global.js": "https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js",
|
||||
"marked.min.js": "https://cdn.jsdelivr.net/npm/marked/marked.min.js",
|
||||
"easymde.min.js": "https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.js",
|
||||
"axios.min.js": "https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js",
|
||||
}
|
||||
}
|
||||
|
||||
# Font Awesome webfonts
|
||||
FONTAWESOME_FONTS = [
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/webfonts/fa-brands-400.ttf",
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/webfonts/fa-brands-400.woff2",
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/webfonts/fa-regular-400.ttf",
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/webfonts/fa-regular-400.woff2",
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/webfonts/fa-solid-900.ttf",
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/webfonts/fa-solid-900.woff2",
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/webfonts/fa-v4compatibility.ttf",
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/webfonts/fa-v4compatibility.woff2",
|
||||
]
|
||||
|
||||
def download_file(url, filepath):
|
||||
"""Download a file from URL to filepath"""
|
||||
print(f"Downloading {url} to {filepath}")
|
||||
try:
|
||||
response = requests.get(url, timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
# Create directory if it doesn't exist
|
||||
filepath.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Write file
|
||||
with open(filepath, 'wb') as f:
|
||||
f.write(response.content)
|
||||
print(f" ✓ Downloaded {filepath.name}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f" ✗ Failed to download {url}: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
print("Downloading offline dependencies...")
|
||||
print(f"Vendor directory: {VENDOR_DIR}")
|
||||
|
||||
# Check if we're in production mode
|
||||
is_production = os.environ.get('FLASK_ENV') == 'production' or os.environ.get('PRODUCTION') == '1'
|
||||
|
||||
if is_production:
|
||||
print("⚙️ PRODUCTION MODE: Using production builds")
|
||||
# Replace Vue.js development build with production build
|
||||
DEPENDENCIES['js']['vue.global.js'] = "https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"
|
||||
else:
|
||||
print("⚙️ DEVELOPMENT MODE: Using development builds")
|
||||
|
||||
# Download CSS and JS files
|
||||
for file_type, files in DEPENDENCIES.items():
|
||||
print(f"\n{file_type.upper()} Files:")
|
||||
for filename, url in files.items():
|
||||
filepath = VENDOR_DIR / file_type / filename
|
||||
download_file(url, filepath)
|
||||
|
||||
# Download Font Awesome fonts
|
||||
print("\nFont Awesome Webfonts:")
|
||||
for url in FONTAWESOME_FONTS:
|
||||
filename = url.split("/")[-1]
|
||||
filepath = VENDOR_DIR / "fonts" / "webfonts" / filename
|
||||
download_file(url, filepath)
|
||||
|
||||
# Update Font Awesome CSS to use local fonts
|
||||
fa_css_path = VENDOR_DIR / "css" / "fontawesome.min.css"
|
||||
if fa_css_path.exists():
|
||||
print("\nUpdating Font Awesome CSS to use local fonts...")
|
||||
with open(fa_css_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Replace CDN URLs with local paths - handle both relative and absolute URLs
|
||||
content = content.replace(
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/webfonts/",
|
||||
"../fonts/webfonts/"
|
||||
)
|
||||
# Also replace any relative URLs that might be in the minified CSS
|
||||
content = content.replace(
|
||||
"../webfonts/",
|
||||
"../fonts/webfonts/"
|
||||
)
|
||||
content = content.replace(
|
||||
"./webfonts/",
|
||||
"../fonts/webfonts/"
|
||||
)
|
||||
|
||||
with open(fa_css_path, 'w') as f:
|
||||
f.write(content)
|
||||
print(" ✓ Updated Font Awesome CSS paths")
|
||||
|
||||
print("\n✅ All dependencies downloaded successfully!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
44
scripts/migrate_docker.sh
Normal file
44
scripts/migrate_docker.sh
Normal file
@@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
# Manual migration script for Docker deployments
|
||||
# NOTE: The first 10 recordings are processed automatically on startup.
|
||||
# This script is for processing any remaining recordings.
|
||||
|
||||
echo "🎯 Inquire Mode Manual Migration for Docker"
|
||||
echo "============================================="
|
||||
|
||||
# Check if container is running
|
||||
if ! docker compose ps | grep -q "speakr.*Up"; then
|
||||
echo "❌ Speakr container is not running. Please start it first with:"
|
||||
echo " docker compose up -d"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "ℹ️ Note: The first 10 recordings are processed automatically on startup."
|
||||
echo "ℹ️ This script processes any remaining recordings that need chunking."
|
||||
echo ""
|
||||
echo "🔍 Checking how many recordings still need processing..."
|
||||
|
||||
# First, do a dry run to see what would be processed
|
||||
docker compose exec app python migrate_existing_recordings.py --dry-run
|
||||
|
||||
echo ""
|
||||
echo "⚠️ Do you want to proceed with processing these recordings?"
|
||||
echo "⚠️ This will create embeddings and may take several minutes."
|
||||
read -p "Continue? (y/N): " -n 1 -r
|
||||
echo
|
||||
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "🚀 Starting migration..."
|
||||
docker compose exec app python migrate_existing_recordings.py --process --batch-size 5
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✅ Migration completed successfully!"
|
||||
echo "🎉 Your existing recordings are now ready for Inquire Mode!"
|
||||
else
|
||||
echo "❌ Migration failed. Check the logs above for details."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "❌ Migration cancelled."
|
||||
exit 0
|
||||
fi
|
||||
142
scripts/migrate_existing_recordings.py
Normal file
142
scripts/migrate_existing_recordings.py
Normal file
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Migration script to process existing recordings for Inquire Mode.
|
||||
This script will chunk and vectorize all existing recordings that haven't been processed yet.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from src.app import app, db, Recording, TranscriptChunk, process_recording_chunks
|
||||
|
||||
def count_recordings_needing_processing():
|
||||
"""Count how many recordings need chunk processing."""
|
||||
with app.app_context():
|
||||
# Get all completed recordings
|
||||
completed_recordings = Recording.query.filter_by(status='COMPLETED').all()
|
||||
|
||||
# Check which ones don't have chunks
|
||||
recordings_needing_processing = []
|
||||
for recording in completed_recordings:
|
||||
if recording.transcription: # Has transcription
|
||||
chunk_count = TranscriptChunk.query.filter_by(recording_id=recording.id).count()
|
||||
if chunk_count == 0: # No chunks yet
|
||||
recordings_needing_processing.append(recording)
|
||||
|
||||
return recordings_needing_processing
|
||||
|
||||
def migrate_existing_recordings(batch_size=10, dry_run=False):
|
||||
"""
|
||||
Process existing recordings in batches to create chunks and embeddings.
|
||||
|
||||
Args:
|
||||
batch_size (int): Number of recordings to process at once
|
||||
dry_run (bool): If True, just show what would be processed
|
||||
"""
|
||||
with app.app_context():
|
||||
recordings_to_process = count_recordings_needing_processing()
|
||||
|
||||
print(f"🔍 Found {len(recordings_to_process)} recordings that need chunk processing")
|
||||
|
||||
if len(recordings_to_process) == 0:
|
||||
print("✅ All recordings are already processed!")
|
||||
return True
|
||||
|
||||
if dry_run:
|
||||
print("\n📋 Recordings that would be processed:")
|
||||
for i, recording in enumerate(recordings_to_process, 1):
|
||||
print(f" {i}. {recording.title} (ID: {recording.id}) - {len(recording.transcription)} chars")
|
||||
print(f"\nThis is a dry run. Use --process to actually run the migration.")
|
||||
return True
|
||||
|
||||
print(f"🚀 Processing {len(recordings_to_process)} recordings in batches of {batch_size}")
|
||||
|
||||
processed = 0
|
||||
errors = 0
|
||||
|
||||
for i in range(0, len(recordings_to_process), batch_size):
|
||||
batch = recordings_to_process[i:i + batch_size]
|
||||
print(f"\n📦 Processing batch {i//batch_size + 1} ({len(batch)} recordings)...")
|
||||
|
||||
for recording in batch:
|
||||
try:
|
||||
print(f" ⏳ Processing: {recording.title} (ID: {recording.id})")
|
||||
|
||||
success = process_recording_chunks(recording.id)
|
||||
if success:
|
||||
processed += 1
|
||||
# Get chunk count to report
|
||||
chunk_count = TranscriptChunk.query.filter_by(recording_id=recording.id).count()
|
||||
print(f" ✅ Created {chunk_count} chunks")
|
||||
else:
|
||||
errors += 1
|
||||
print(f" ❌ Failed to process recording {recording.id}")
|
||||
|
||||
except Exception as e:
|
||||
errors += 1
|
||||
print(f" ❌ Error processing recording {recording.id}: {e}")
|
||||
|
||||
# Commit batch
|
||||
try:
|
||||
db.session.commit()
|
||||
print(f" 💾 Batch committed successfully")
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
print(f" ❌ Error committing batch: {e}")
|
||||
errors += len(batch)
|
||||
|
||||
print(f"\n📊 Migration Summary:")
|
||||
print(f" ✅ Successfully processed: {processed}")
|
||||
print(f" ❌ Errors: {errors}")
|
||||
print(f" 📈 Success rate: {(processed/(processed+errors)*100):.1f}%" if (processed+errors) > 0 else "N/A")
|
||||
|
||||
return errors == 0
|
||||
|
||||
def main():
|
||||
"""Main function to handle command line arguments."""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description='Migrate existing recordings for Inquire Mode')
|
||||
parser.add_argument('--dry-run', action='store_true',
|
||||
help='Show what would be processed without actually processing')
|
||||
parser.add_argument('--process', action='store_true',
|
||||
help='Actually process the recordings')
|
||||
parser.add_argument('--batch-size', type=int, default=10,
|
||||
help='Number of recordings to process in each batch (default: 10)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.dry_run and not args.process:
|
||||
print("❌ Please specify either --dry-run or --process")
|
||||
print("Use --help for more information")
|
||||
return False
|
||||
|
||||
print("🎯 Inquire Mode Migration Tool")
|
||||
print("=" * 40)
|
||||
|
||||
try:
|
||||
if args.dry_run:
|
||||
success = migrate_existing_recordings(args.batch_size, dry_run=True)
|
||||
else:
|
||||
print("⚠️ This will process all existing recordings and create embeddings.")
|
||||
print("⚠️ This may take a while and use significant CPU/memory.")
|
||||
|
||||
confirm = input("Continue? (y/N): ")
|
||||
if confirm.lower() != 'y':
|
||||
print("❌ Migration cancelled by user")
|
||||
return False
|
||||
|
||||
success = migrate_existing_recordings(args.batch_size, dry_run=False)
|
||||
|
||||
return success
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n❌ Migration cancelled by user")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ Migration failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = main()
|
||||
sys.exit(0 if success else 1)
|
||||
137
scripts/migrate_team_to_group.py
Executable file
137
scripts/migrate_team_to_group.py
Executable file
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Migration script to rename team tables to group tables.
|
||||
This handles the refactoring from team-based to group-based terminology.
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add the parent directory to the path to import app modules
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||
|
||||
from src.app import app, db
|
||||
from sqlalchemy import text
|
||||
|
||||
def migrate_tables():
|
||||
"""Copy data from team tables to group tables and remove old tables."""
|
||||
with app.app_context():
|
||||
try:
|
||||
# Check if old tables exist
|
||||
inspector = db.inspect(db.engine)
|
||||
existing_tables = inspector.get_table_names()
|
||||
|
||||
print("Existing tables:", existing_tables)
|
||||
|
||||
# Check if we need to migrate data
|
||||
if 'team' in existing_tables and 'group' in existing_tables:
|
||||
# Both tables exist - need to copy data
|
||||
print("\nBoth 'team' and 'group' tables exist. Copying data...")
|
||||
|
||||
# Check if there's data in the old table
|
||||
result = db.session.execute(text('SELECT COUNT(*) FROM team'))
|
||||
old_count = result.scalar()
|
||||
print(f"Found {old_count} records in 'team' table")
|
||||
|
||||
if old_count > 0:
|
||||
# Copy data from team to group
|
||||
print("Copying data from 'team' to 'group'...")
|
||||
db.session.execute(text(
|
||||
'INSERT INTO "group" (id, name, description, created_at) '
|
||||
'SELECT id, name, description, created_at FROM team'
|
||||
))
|
||||
db.session.commit()
|
||||
print(f"✓ Copied {old_count} records to 'group' table")
|
||||
|
||||
# Drop the old team table
|
||||
print("Dropping old 'team' table...")
|
||||
db.session.execute(text('DROP TABLE team'))
|
||||
db.session.commit()
|
||||
print("✓ Dropped old 'team' table")
|
||||
|
||||
elif 'team' in existing_tables and 'group' not in existing_tables:
|
||||
# Only old table exists - rename it
|
||||
print("\nRenaming 'team' table to 'group'...")
|
||||
db.session.execute(text('ALTER TABLE team RENAME TO "group"'))
|
||||
db.session.commit()
|
||||
print("✓ Renamed 'team' to 'group'")
|
||||
else:
|
||||
print("\n'team' table not found or already migrated")
|
||||
|
||||
# Migrate team_membership
|
||||
if 'team_membership' in existing_tables and 'group_membership' in existing_tables:
|
||||
# Both tables exist - need to copy data
|
||||
print("\nBoth 'team_membership' and 'group_membership' tables exist. Copying data...")
|
||||
|
||||
# Check if there's data in the old table
|
||||
result = db.session.execute(text('SELECT COUNT(*) FROM team_membership'))
|
||||
old_count = result.scalar()
|
||||
print(f"Found {old_count} records in 'team_membership' table")
|
||||
|
||||
if old_count > 0:
|
||||
# Copy data from team_membership to group_membership
|
||||
print("Copying data from 'team_membership' to 'group_membership'...")
|
||||
db.session.execute(text(
|
||||
'INSERT INTO group_membership (id, user_id, group_id, role, joined_at) '
|
||||
'SELECT id, user_id, team_id, role, joined_at FROM team_membership'
|
||||
))
|
||||
db.session.commit()
|
||||
print(f"✓ Copied {old_count} records to 'group_membership' table")
|
||||
|
||||
# Drop the old team_membership table
|
||||
print("Dropping old 'team_membership' table...")
|
||||
db.session.execute(text('DROP TABLE team_membership'))
|
||||
db.session.commit()
|
||||
print("✓ Dropped old 'team_membership' table")
|
||||
|
||||
elif 'team_membership' in existing_tables and 'group_membership' not in existing_tables:
|
||||
# Only old table exists - rename it
|
||||
print("\nRenaming 'team_membership' table to 'group_membership'...")
|
||||
db.session.execute(text('ALTER TABLE team_membership RENAME TO group_membership'))
|
||||
db.session.commit()
|
||||
print("✓ Renamed 'team_membership' to 'group_membership'")
|
||||
else:
|
||||
print("\n'team_membership' table not found or already migrated")
|
||||
|
||||
# Migrate team_id to group_id in tags table
|
||||
print("\nMigrating tag associations from team_id to group_id...")
|
||||
result = db.session.execute(text(
|
||||
'UPDATE tag SET group_id = team_id WHERE team_id IS NOT NULL AND group_id IS NULL'
|
||||
))
|
||||
db.session.commit()
|
||||
print(f"✓ Migrated {result.rowcount} tag associations")
|
||||
|
||||
# Migrate share_with_team_lead to share_with_group_lead in tags
|
||||
result = db.session.execute(text(
|
||||
'UPDATE tag SET share_with_group_lead = share_with_team_lead WHERE share_with_team_lead IS NOT NULL AND share_with_group_lead IS NULL'
|
||||
))
|
||||
db.session.commit()
|
||||
print(f"✓ Migrated {result.rowcount} share_with_lead settings")
|
||||
|
||||
print("\n✅ Migration completed successfully!")
|
||||
print("\nPlease restart the application for changes to take full effect.")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ Error during migration: {e}")
|
||||
db.session.rollback()
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("=" * 60)
|
||||
print("Team to Group Migration Script")
|
||||
print("=" * 60)
|
||||
print("\nThis script will rename database tables:")
|
||||
print(" - 'team' → 'group'")
|
||||
print(" - 'team_membership' → 'group_membership'")
|
||||
|
||||
# Check for --yes flag to skip confirmation
|
||||
if '--yes' in sys.argv or '-y' in sys.argv:
|
||||
print("\nAuto-confirming migration (--yes flag detected)...\n")
|
||||
else:
|
||||
print("\nPress Ctrl+C to cancel, or Enter to continue...")
|
||||
try:
|
||||
input()
|
||||
except KeyboardInterrupt:
|
||||
print("\n\nMigration cancelled.")
|
||||
sys.exit(0)
|
||||
|
||||
migrate_tables()
|
||||
219
scripts/parse_asr_json.py
Executable file
219
scripts/parse_asr_json.py
Executable file
@@ -0,0 +1,219 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ASR JSON Parser - Analyzes speaker information in ASR response JSON files
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from collections import defaultdict, Counter
|
||||
|
||||
def analyze_asr_json(json_data):
|
||||
"""
|
||||
Analyze ASR JSON data to understand speaker distribution and identify issues
|
||||
"""
|
||||
if not isinstance(json_data, dict) or 'segments' not in json_data:
|
||||
print("ERROR: Invalid JSON structure. Expected dict with 'segments' key.")
|
||||
return
|
||||
|
||||
segments = json_data['segments']
|
||||
if not isinstance(segments, list):
|
||||
print("ERROR: 'segments' should be a list.")
|
||||
return
|
||||
|
||||
print(f"=== ASR JSON Analysis ===")
|
||||
print(f"Total segments: {len(segments)}")
|
||||
print()
|
||||
|
||||
# Track segment-level speakers
|
||||
segment_speakers = []
|
||||
segments_with_speaker = 0
|
||||
segments_without_speaker = 0
|
||||
|
||||
# Track word-level speakers
|
||||
word_speakers = []
|
||||
words_with_speaker = 0
|
||||
words_without_speaker = 0
|
||||
|
||||
# Track segments with null speakers
|
||||
null_speaker_segments = []
|
||||
|
||||
for i, segment in enumerate(segments):
|
||||
# Analyze segment-level speaker
|
||||
segment_speaker = segment.get('speaker')
|
||||
if segment_speaker is not None:
|
||||
segment_speakers.append(segment_speaker)
|
||||
segments_with_speaker += 1
|
||||
else:
|
||||
segments_without_speaker += 1
|
||||
null_speaker_segments.append(i)
|
||||
|
||||
# Analyze word-level speakers
|
||||
words = segment.get('words', [])
|
||||
for word_data in words:
|
||||
word_speaker = word_data.get('speaker')
|
||||
if word_speaker is not None:
|
||||
word_speakers.append(word_speaker)
|
||||
words_with_speaker += 1
|
||||
else:
|
||||
words_without_speaker += 1
|
||||
|
||||
# Print segment-level analysis
|
||||
print("=== SEGMENT-LEVEL SPEAKERS ===")
|
||||
print(f"Segments with speakers: {segments_with_speaker}")
|
||||
print(f"Segments without speakers: {segments_without_speaker}")
|
||||
|
||||
if segment_speakers:
|
||||
segment_speaker_counts = Counter(segment_speakers)
|
||||
print(f"Unique segment speakers: {sorted(segment_speaker_counts.keys())}")
|
||||
print("Segment speaker distribution:")
|
||||
for speaker, count in segment_speaker_counts.most_common():
|
||||
print(f" {speaker}: {count} segments")
|
||||
else:
|
||||
print("No segment-level speakers found!")
|
||||
|
||||
print()
|
||||
|
||||
# Print word-level analysis
|
||||
print("=== WORD-LEVEL SPEAKERS ===")
|
||||
print(f"Words with speakers: {words_with_speaker}")
|
||||
print(f"Words without speakers: {words_without_speaker}")
|
||||
|
||||
if word_speakers:
|
||||
word_speaker_counts = Counter(word_speakers)
|
||||
print(f"Unique word speakers: {sorted(word_speaker_counts.keys())}")
|
||||
print("Word speaker distribution:")
|
||||
for speaker, count in word_speaker_counts.most_common():
|
||||
print(f" {speaker}: {count} words")
|
||||
else:
|
||||
print("No word-level speakers found!")
|
||||
|
||||
print()
|
||||
|
||||
# Analyze segments without speakers
|
||||
if null_speaker_segments:
|
||||
print("=== SEGMENTS WITHOUT SPEAKERS ===")
|
||||
print(f"Segment indices without speakers: {null_speaker_segments[:10]}{'...' if len(null_speaker_segments) > 10 else ''}")
|
||||
|
||||
print("\nFirst few segments without speakers:")
|
||||
for i in null_speaker_segments[:5]:
|
||||
segment = segments[i]
|
||||
text = segment.get('text', '').strip()
|
||||
start = segment.get('start')
|
||||
end = segment.get('end')
|
||||
words = segment.get('words', [])
|
||||
|
||||
print(f" Segment {i}: '{text}' ({start}-{end}s)")
|
||||
print(f" Keys: {list(segment.keys())}")
|
||||
|
||||
# Check if words have speakers even when segment doesn't
|
||||
word_speakers_in_segment = [w.get('speaker') for w in words if w.get('speaker')]
|
||||
if word_speakers_in_segment:
|
||||
word_speaker_counts = Counter(word_speakers_in_segment)
|
||||
print(f" Word speakers: {dict(word_speaker_counts)}")
|
||||
else:
|
||||
print(f" No word speakers either")
|
||||
print()
|
||||
|
||||
# Suggest speaker assignment strategy
|
||||
print("=== SPEAKER ASSIGNMENT STRATEGY ===")
|
||||
if segments_without_speaker > 0:
|
||||
print(f"Found {segments_without_speaker} segments without speakers.")
|
||||
|
||||
if words_with_speaker > 0:
|
||||
print("RECOMMENDATION: Use word-level speaker information to assign segment speakers.")
|
||||
print("Strategy: For segments without speakers, find the most common speaker among their words.")
|
||||
else:
|
||||
print("RECOMMENDATION: Assign a default speaker label (e.g., 'UNKNOWN_SPEAKER') to segments without speakers.")
|
||||
else:
|
||||
print("All segments have speakers assigned. No action needed.")
|
||||
|
||||
def suggest_preprocessing_fix(json_data):
|
||||
"""
|
||||
Suggest how to fix the preprocessing based on the JSON structure
|
||||
"""
|
||||
print("\n=== PREPROCESSING FIX SUGGESTION ===")
|
||||
|
||||
segments = json_data.get('segments', [])
|
||||
if not segments:
|
||||
return
|
||||
|
||||
# Check if we can derive segment speakers from word speakers
|
||||
segments_fixable = 0
|
||||
for segment in segments:
|
||||
if segment.get('speaker') is None:
|
||||
words = segment.get('words', [])
|
||||
word_speakers = [w.get('speaker') for w in words if w.get('speaker')]
|
||||
if word_speakers:
|
||||
segments_fixable += 1
|
||||
|
||||
if segments_fixable > 0:
|
||||
print(f"✅ {segments_fixable} segments can be fixed using word-level speaker information.")
|
||||
print("\nSuggested code fix:")
|
||||
print("""
|
||||
# In the ASR processing function, replace the segment processing with:
|
||||
for i, segment in enumerate(asr_response_data['segments']):
|
||||
speaker = segment.get('speaker')
|
||||
text = segment.get('text', '').strip()
|
||||
|
||||
# If segment doesn't have a speaker, try to derive from words
|
||||
if speaker is None:
|
||||
words = segment.get('words', [])
|
||||
word_speakers = [w.get('speaker') for w in words if w.get('speaker')]
|
||||
if word_speakers:
|
||||
# Use the most common speaker among the words
|
||||
from collections import Counter
|
||||
speaker_counts = Counter(word_speakers)
|
||||
speaker = speaker_counts.most_common(1)[0][0]
|
||||
app.logger.info(f"Derived speaker '{speaker}' for segment {i} from word-level data")
|
||||
else:
|
||||
speaker = 'UNKNOWN_SPEAKER'
|
||||
app.logger.warning(f"No speaker info available for segment {i}, using UNKNOWN_SPEAKER")
|
||||
|
||||
simplified_segments.append({
|
||||
'speaker': speaker,
|
||||
'sentence': text,
|
||||
'start_time': segment.get('start'),
|
||||
'end_time': segment.get('end')
|
||||
})
|
||||
""")
|
||||
else:
|
||||
print("❌ Segments cannot be fixed using word-level data.")
|
||||
print("Recommendation: Assign 'UNKNOWN_SPEAKER' to segments without speakers.")
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: python parse_asr_json.py <json_file>")
|
||||
print(" or: python parse_asr_json.py -")
|
||||
print(" (use '-' to read from stdin)")
|
||||
sys.exit(1)
|
||||
|
||||
filename = sys.argv[1]
|
||||
|
||||
try:
|
||||
if filename == '-':
|
||||
# Read from stdin
|
||||
json_text = sys.stdin.read()
|
||||
else:
|
||||
# Read from file
|
||||
with open(filename, 'r', encoding='utf-8') as f:
|
||||
json_text = f.read()
|
||||
|
||||
# Parse JSON
|
||||
json_data = json.loads(json_text)
|
||||
|
||||
# Analyze the data
|
||||
analyze_asr_json(json_data)
|
||||
suggest_preprocessing_fix(json_data)
|
||||
|
||||
except FileNotFoundError:
|
||||
print(f"ERROR: File '{filename}' not found.")
|
||||
sys.exit(1)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"ERROR: Invalid JSON - {e}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"ERROR: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
21
scripts/pre-commit
Executable file
21
scripts/pre-commit
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Pre-commit hook: runs PostgreSQL migration compatibility tests
|
||||
# when migration-related files are staged.
|
||||
#
|
||||
# Install: ln -sf ../../scripts/pre-commit .git/hooks/pre-commit
|
||||
|
||||
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
|
||||
|
||||
# Only run if migration-related files are staged
|
||||
if echo "$STAGED_FILES" | grep -qE '^(src/init_db\.py|src/utils/database\.py|src/models/)'; then
|
||||
echo "Migration files changed — running PostgreSQL compatibility checks..."
|
||||
python tests/test_migration_compatibility.py
|
||||
if [ $? -ne 0 ]; then
|
||||
echo ""
|
||||
echo "Pre-commit hook FAILED: PostgreSQL migration compatibility issues found."
|
||||
echo "Fix the issues above before committing."
|
||||
exit 1
|
||||
fi
|
||||
echo "All migration compatibility checks passed."
|
||||
fi
|
||||
130
scripts/reset_db.py
Normal file
130
scripts/reset_db.py
Normal file
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Add this near the top if you run this standalone often outside app context
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
# Add project root to path if necessary for 'app' import
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||
|
||||
# Load environment variables in case DB path relies on them (optional here)
|
||||
# from dotenv import load_dotenv
|
||||
# load_dotenv()
|
||||
|
||||
# Check if running within app context already (e.g., via Flask command)
|
||||
try:
|
||||
from flask import current_app
|
||||
# Ensure app context is pushed if needed for config access
|
||||
app = current_app._get_current_object()
|
||||
# Make sure db is initialized within the app context if needed
|
||||
# (SQLAlchemy initialization in app.py handles this mostly)
|
||||
with app.app_context():
|
||||
db = app.extensions['sqlalchemy'].db # Access db via extensions
|
||||
except (RuntimeError, AttributeError, KeyError):
|
||||
# If not in app context, import directly
|
||||
try:
|
||||
# Ensure this import reflects the updated app.py with the new model
|
||||
from src.app import app, db
|
||||
except ImportError as e:
|
||||
print(f"Error: Could not import 'app' and 'db': {e}")
|
||||
print("Make sure reset_db.py is runnable and PYTHONPATH is set.")
|
||||
sys.exit(1)
|
||||
|
||||
def reset_database(delete_uploads=True):
|
||||
# Determine the database path relative to the instance folder
|
||||
# Use app config if available
|
||||
instance_path = app.instance_path if hasattr(app, 'instance_path') else os.path.join(os.getcwd(), 'instance')
|
||||
try:
|
||||
# Ensure app context for config access if not already present
|
||||
with app.app_context():
|
||||
# Use absolute path from config
|
||||
db_uri = app.config.get('SQLALCHEMY_DATABASE_URI', 'sqlite:///instance/transcriptions.db')
|
||||
# Handle relative vs absolute paths specified in URI
|
||||
if db_uri.startswith('sqlite:///'):
|
||||
# Assume absolute path from URI root if starts with '///'
|
||||
db_path = db_uri.replace('sqlite:///', '/', 1) # Replace only first
|
||||
# Ensure instance path reflects the directory containing the DB
|
||||
instance_path = os.path.dirname(db_path)
|
||||
elif db_uri.startswith('sqlite://'):
|
||||
# Assume relative path from instance folder
|
||||
db_filename = db_uri.split('/')[-1]
|
||||
db_path = os.path.join(instance_path, db_filename)
|
||||
else: # Handle other DB types or formats if needed
|
||||
print(f"Warning: Non-SQLite URI detected: {db_uri}. Deletion logic might need adjustment.")
|
||||
# Attempt to parse or fallback
|
||||
db_filename = db_uri.split('/')[-1] # Best guess
|
||||
db_path = os.path.join(instance_path, db_filename)
|
||||
|
||||
except Exception as config_e:
|
||||
print(f"Error accessing app config for DB path: {config_e}. Using default.")
|
||||
# Fallback if config access fails
|
||||
instance_path = os.path.join(os.getcwd(), 'instance')
|
||||
db_filename = 'transcriptions.db'
|
||||
db_path = os.path.join(instance_path, db_filename)
|
||||
|
||||
# Ensure instance directory exists
|
||||
print(f"Ensuring instance directory exists: {instance_path}")
|
||||
os.makedirs(instance_path, exist_ok=True)
|
||||
print(f"Database path identified as: {db_path}")
|
||||
|
||||
# Remove existing database if it exists
|
||||
if os.path.exists(db_path):
|
||||
print(f"Removing existing database at {db_path}")
|
||||
try:
|
||||
os.remove(db_path)
|
||||
# Also remove journal file if it exists
|
||||
journal_path = db_path + "-journal"
|
||||
if os.path.exists(journal_path):
|
||||
os.remove(journal_path)
|
||||
print(f"Removing existing journal file at {journal_path}")
|
||||
except OSError as e:
|
||||
print(f"Error removing database file: {e}. Check permissions or if it's in use.")
|
||||
# Decide whether to exit or continue
|
||||
# sys.exit(1)
|
||||
|
||||
# Create application context to work with the database
|
||||
try:
|
||||
with app.app_context():
|
||||
print("Creating new database schema (including 'summary' column)...")
|
||||
# Create all tables defined in models (app.py)
|
||||
db.create_all()
|
||||
print("Database schema created successfully!")
|
||||
except Exception as e:
|
||||
print(f"Error creating database schema: {e}")
|
||||
# Attempt rollback if possible (though less relevant for create_all)
|
||||
try:
|
||||
db.session.rollback()
|
||||
except Exception as rb_e:
|
||||
print(f"Rollback attempt failed: {rb_e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Delete all files in the uploads directory if requested
|
||||
if delete_uploads:
|
||||
try:
|
||||
uploads_dir = os.path.join(os.getcwd(), 'uploads')
|
||||
if os.path.exists(uploads_dir):
|
||||
print(f"Deleting all files in uploads directory: {uploads_dir}")
|
||||
for filename in os.listdir(uploads_dir):
|
||||
file_path = os.path.join(uploads_dir, filename)
|
||||
try:
|
||||
if os.path.isfile(file_path):
|
||||
os.remove(file_path)
|
||||
print(f"Deleted file: {file_path}")
|
||||
elif os.path.isdir(file_path):
|
||||
shutil.rmtree(file_path)
|
||||
print(f"Deleted directory: {file_path}")
|
||||
except Exception as e:
|
||||
print(f"Error deleting {file_path}: {e}")
|
||||
print("All files in uploads directory have been deleted.")
|
||||
else:
|
||||
print(f"Uploads directory not found: {uploads_dir}")
|
||||
# Create the directory if it doesn't exist
|
||||
os.makedirs(uploads_dir, exist_ok=True)
|
||||
print(f"Created uploads directory: {uploads_dir}")
|
||||
except Exception as e:
|
||||
print(f"Error cleaning uploads directory: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Attempting to reset the database and clean up all data...")
|
||||
reset_database(delete_uploads=True)
|
||||
print("Database reset process finished.")
|
||||
163
scripts/resize_logo.py
Normal file
163
scripts/resize_logo.py
Normal file
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Logo Resizer Script for Speakr
|
||||
Resizes a source PNG image to all required icon sizes for PWA and favicon support.
|
||||
|
||||
Usage:
|
||||
python resize_logo.py <source_image.png>
|
||||
|
||||
Requirements:
|
||||
pip install Pillow
|
||||
|
||||
This script will create all the necessary icon sizes in the static/img/ directory.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
from PIL import Image, ImageDraw
|
||||
import argparse
|
||||
|
||||
def create_maskable_version(image, size):
|
||||
"""Create a maskable version with safe zone padding (20% on all sides)"""
|
||||
# Calculate the size of the logo with padding
|
||||
logo_size = int(size * 0.6) # Logo takes 60% of the canvas (20% padding on each side)
|
||||
|
||||
# Create new image with transparent background
|
||||
maskable = Image.new('RGBA', (size, size), (0, 0, 0, 0))
|
||||
|
||||
# Resize the original logo
|
||||
logo_resized = image.resize((logo_size, logo_size), Image.Resampling.LANCZOS)
|
||||
|
||||
# Calculate position to center the logo
|
||||
x = (size - logo_size) // 2
|
||||
y = (size - logo_size) // 2
|
||||
|
||||
# Paste the logo onto the center of the canvas
|
||||
maskable.paste(logo_resized, (x, y), logo_resized if logo_resized.mode == 'RGBA' else None)
|
||||
|
||||
return maskable
|
||||
|
||||
def resize_logo(source_path, output_dir="static/img"):
|
||||
"""Resize the source image to all required sizes"""
|
||||
|
||||
# Check if source file exists
|
||||
if not os.path.exists(source_path):
|
||||
print(f"Error: Source file '{source_path}' not found!")
|
||||
return False
|
||||
|
||||
# Create output directory if it doesn't exist
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
try:
|
||||
# Open the source image
|
||||
with Image.open(source_path) as img:
|
||||
# Convert to RGBA if not already (for transparency support)
|
||||
if img.mode != 'RGBA':
|
||||
img = img.convert('RGBA')
|
||||
|
||||
print(f"Source image: {img.size[0]}x{img.size[1]} pixels")
|
||||
print(f"Output directory: {output_dir}")
|
||||
print()
|
||||
|
||||
# Define all the sizes we need
|
||||
sizes = {
|
||||
# Essential PWA icons
|
||||
'icon-192x192.png': 192,
|
||||
'icon-512x512.png': 512,
|
||||
|
||||
# Additional recommended icons
|
||||
'icon-16x16.png': 16,
|
||||
'icon-32x32.png': 32,
|
||||
'icon-180x180.png': 180, # Apple touch icon
|
||||
|
||||
# Maskable version
|
||||
'icon-maskable-512x512.png': 512,
|
||||
}
|
||||
|
||||
# Resize to each required size
|
||||
for filename, size in sizes.items():
|
||||
output_path = os.path.join(output_dir, filename)
|
||||
|
||||
if 'maskable' in filename:
|
||||
# Create maskable version with safe zone
|
||||
resized = create_maskable_version(img, size)
|
||||
print(f"✓ Created maskable icon: {filename} ({size}x{size})")
|
||||
else:
|
||||
# Regular resize
|
||||
resized = img.resize((size, size), Image.Resampling.LANCZOS)
|
||||
print(f"✓ Created icon: {filename} ({size}x{size})")
|
||||
|
||||
# Save the resized image
|
||||
resized.save(output_path, 'PNG', optimize=True)
|
||||
|
||||
print()
|
||||
print("🎉 All icons created successfully!")
|
||||
print()
|
||||
print("Next steps:")
|
||||
print("1. Replace static/img/favicon.svg with your SVG version (if you have one)")
|
||||
print("2. Clear browser cache and test the new icons")
|
||||
print("3. Test PWA installation to verify icons appear correctly")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error processing image: {e}")
|
||||
return False
|
||||
|
||||
def create_ico_favicon(source_path, output_dir="static/img"):
|
||||
"""Create a multi-size ICO favicon file"""
|
||||
try:
|
||||
with Image.open(source_path) as img:
|
||||
if img.mode != 'RGBA':
|
||||
img = img.convert('RGBA')
|
||||
|
||||
# Create different sizes for the ICO file
|
||||
sizes = [16, 32, 48]
|
||||
images = []
|
||||
|
||||
for size in sizes:
|
||||
resized = img.resize((size, size), Image.Resampling.LANCZOS)
|
||||
images.append(resized)
|
||||
|
||||
# Save as ICO file
|
||||
ico_path = os.path.join(output_dir, 'favicon.ico')
|
||||
images[0].save(ico_path, format='ICO', sizes=[(img.width, img.height) for img in images])
|
||||
print(f"✓ Created favicon.ico with sizes: {sizes}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not create favicon.ico: {e}")
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Resize logo for Speakr PWA icons')
|
||||
parser.add_argument('source', help='Source PNG image file')
|
||||
parser.add_argument('--output-dir', default='static/img', help='Output directory (default: static/img)')
|
||||
parser.add_argument('--create-ico', action='store_true', help='Also create favicon.ico file')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
print("🎨 Speakr Logo Resizer")
|
||||
print("=" * 50)
|
||||
|
||||
# Resize the logo
|
||||
success = resize_logo(args.source, args.output_dir)
|
||||
|
||||
if success and args.create_ico:
|
||||
print()
|
||||
create_ico_favicon(args.source, args.output_dir)
|
||||
|
||||
if success:
|
||||
print()
|
||||
print("📁 Files created in", args.output_dir + ":")
|
||||
for file in os.listdir(args.output_dir):
|
||||
if file.startswith('icon-') and file.endswith('.png'):
|
||||
file_path = os.path.join(args.output_dir, file)
|
||||
size = os.path.getsize(file_path)
|
||||
print(f" {file} ({size:,} bytes)")
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python resize_logo.py <source_image.png>")
|
||||
print("Example: python resize_logo.py my_logo.png")
|
||||
sys.exit(1)
|
||||
|
||||
main()
|
||||
83
scripts/resize_logo.sh
Executable file
83
scripts/resize_logo.sh
Executable file
@@ -0,0 +1,83 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Logo Resizer Script for Speakr (ImageMagick version)
|
||||
# Resizes a source PNG image to all required icon sizes for PWA and favicon support.
|
||||
#
|
||||
# Usage: ./resize_logo.sh <source_image.png>
|
||||
# Requirements: ImageMagick (sudo apt install imagemagick)
|
||||
|
||||
set -e
|
||||
|
||||
# Check if source file is provided
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Usage: $0 <source_image.png>"
|
||||
echo "Example: $0 my_logo.png"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SOURCE_FILE="$1"
|
||||
OUTPUT_DIR="static/img"
|
||||
|
||||
# Check if source file exists
|
||||
if [ ! -f "$SOURCE_FILE" ]; then
|
||||
echo "Error: Source file '$SOURCE_FILE' not found!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if ImageMagick is installed
|
||||
if ! command -v convert &> /dev/null; then
|
||||
echo "Error: ImageMagick is not installed!"
|
||||
echo "Install it with: sudo apt install imagemagick"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create output directory if it doesn't exist
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
echo "🎨 Speakr Logo Resizer (ImageMagick)"
|
||||
echo "=================================================="
|
||||
echo "Source file: $SOURCE_FILE"
|
||||
echo "Output directory: $OUTPUT_DIR"
|
||||
echo
|
||||
|
||||
# Define sizes
|
||||
declare -A SIZES=(
|
||||
["icon-16x16.png"]=16
|
||||
["icon-32x32.png"]=32
|
||||
["icon-180x180.png"]=180
|
||||
["icon-192x192.png"]=192
|
||||
["icon-512x512.png"]=512
|
||||
)
|
||||
|
||||
# Resize to each size
|
||||
for filename in "${!SIZES[@]}"; do
|
||||
size=${SIZES[$filename]}
|
||||
output_path="$OUTPUT_DIR/$filename"
|
||||
|
||||
convert "$SOURCE_FILE" -resize "${size}x${size}" "$output_path"
|
||||
echo "✓ Created icon: $filename (${size}x${size})"
|
||||
done
|
||||
|
||||
# Create maskable version with padding
|
||||
echo "✓ Creating maskable icon with safe zone..."
|
||||
convert "$SOURCE_FILE" -resize 307x307 -gravity center -extent 512x512 -background transparent "$OUTPUT_DIR/icon-maskable-512x512.png"
|
||||
echo "✓ Created maskable icon: icon-maskable-512x512.png (512x512)"
|
||||
|
||||
# Create favicon.ico (optional)
|
||||
if command -v convert &> /dev/null; then
|
||||
echo "✓ Creating favicon.ico..."
|
||||
convert "$SOURCE_FILE" -resize 16x16 -resize 32x32 -resize 48x48 "$OUTPUT_DIR/favicon.ico"
|
||||
echo "✓ Created favicon.ico"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "🎉 All icons created successfully!"
|
||||
echo
|
||||
echo "📁 Files created:"
|
||||
ls -la "$OUTPUT_DIR"/icon-*.png "$OUTPUT_DIR"/favicon.ico 2>/dev/null || true
|
||||
|
||||
echo
|
||||
echo "Next steps:"
|
||||
echo "1. Replace static/img/favicon.svg with your SVG version (if you have one)"
|
||||
echo "2. Clear browser cache and test the new icons"
|
||||
echo "3. Test PWA installation to verify icons appear correctly"
|
||||
44
scripts/test-docs-build.sh
Executable file
44
scripts/test-docs-build.sh
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Test script to validate documentation build locally
|
||||
# This mimics what the GitHub Actions workflow does
|
||||
|
||||
set -e
|
||||
|
||||
echo "Testing documentation build..."
|
||||
|
||||
# Check if we're in the right directory
|
||||
if [ ! -f "docs/mkdocs.yml" ]; then
|
||||
echo "Error: docs/mkdocs.yml not found. Run this script from the project root."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create a virtual environment for testing
|
||||
echo "Creating virtual environment..."
|
||||
python3 -m venv .venv-docs-test
|
||||
source .venv-docs-test/bin/activate
|
||||
|
||||
# Install dependencies
|
||||
echo "Installing dependencies..."
|
||||
pip install --upgrade pip
|
||||
pip install -r docs/requirements-docs.txt
|
||||
|
||||
# Build the documentation
|
||||
echo "Building documentation..."
|
||||
cd docs
|
||||
export CI=true # Enable git plugin in CI mode
|
||||
mkdocs build --strict --site-dir _test_site
|
||||
|
||||
echo ""
|
||||
echo "✅ Documentation build successful!"
|
||||
echo "Built site is in: docs/_test_site"
|
||||
echo ""
|
||||
echo "To serve locally for testing:"
|
||||
echo " cd docs && mkdocs serve"
|
||||
|
||||
# Cleanup
|
||||
cd ..
|
||||
deactivate
|
||||
rm -rf .venv-docs-test
|
||||
|
||||
echo "Cleanup complete."
|
||||
60
scripts/update_version.py
Normal file
60
scripts/update_version.py
Normal file
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple script to update the VERSION file.
|
||||
Usage: python update_version.py v0.4.3
|
||||
"""
|
||||
import sys
|
||||
import re
|
||||
|
||||
def update_version(new_version):
|
||||
# Validate version format (basic check)
|
||||
if not re.match(r'^v?\d+\.\d+\.\d+', new_version):
|
||||
print(f"Warning: Version '{new_version}' doesn't follow standard format (v1.2.3)")
|
||||
|
||||
# Ensure version starts with 'v'
|
||||
if not new_version.startswith('v'):
|
||||
new_version = 'v' + new_version
|
||||
|
||||
# Write to VERSION file
|
||||
try:
|
||||
with open('VERSION', 'w') as f:
|
||||
f.write(new_version)
|
||||
print(f"✅ Updated VERSION file to: {new_version}")
|
||||
|
||||
# Optional: Create git tag if in a git repo
|
||||
import subprocess
|
||||
try:
|
||||
# Check if we're in a git repo
|
||||
subprocess.check_output(['git', 'status'], stderr=subprocess.DEVNULL)
|
||||
|
||||
# Create and push tag
|
||||
subprocess.check_output(['git', 'tag', new_version], stderr=subprocess.DEVNULL)
|
||||
print(f"✅ Created git tag: {new_version}")
|
||||
|
||||
# Ask user if they want to push
|
||||
response = input("Push tag to remote? (y/N): ").strip().lower()
|
||||
if response == 'y':
|
||||
subprocess.check_output(['git', 'push', 'origin', new_version])
|
||||
print(f"✅ Pushed tag {new_version} to remote")
|
||||
|
||||
except subprocess.CalledProcessError:
|
||||
print("ℹ️ Not in a git repo or git tag already exists")
|
||||
except Exception as e:
|
||||
print(f"ℹ️ Git operations failed: {e}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to update VERSION file: {e}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: python update_version.py <version>")
|
||||
print("Example: python update_version.py v0.4.3")
|
||||
print("Example: python update_version.py 0.4.3-alpha")
|
||||
sys.exit(1)
|
||||
|
||||
new_version = sys.argv[1]
|
||||
success = update_version(new_version)
|
||||
sys.exit(0 if success else 1)
|
||||
0
src/api/__init__.py
Normal file
0
src/api/__init__.py
Normal file
1157
src/api/admin.py
Normal file
1157
src/api/admin.py
Normal file
File diff suppressed because it is too large
Load Diff
2259
src/api/api_v1.py
Normal file
2259
src/api/api_v1.py
Normal file
File diff suppressed because it is too large
Load Diff
107
src/api/audit.py
Normal file
107
src/api/audit.py
Normal file
@@ -0,0 +1,107 @@
|
||||
"""
|
||||
Admin API endpoints for audit log queries.
|
||||
|
||||
Loi 25 compliance: allows administrators to review access and auth logs.
|
||||
"""
|
||||
|
||||
from flask import Blueprint, request, jsonify
|
||||
|
||||
from src.services.audit import get_access_logs, get_auth_logs, is_audit_enabled
|
||||
from src.utils import admin_required
|
||||
|
||||
audit_bp = Blueprint('audit', __name__)
|
||||
|
||||
|
||||
@audit_bp.route('/api/admin/audit/status', methods=['GET'])
|
||||
@admin_required
|
||||
def audit_status():
|
||||
"""Check if audit logging is enabled."""
|
||||
return jsonify({'enabled': is_audit_enabled()})
|
||||
|
||||
|
||||
@audit_bp.route('/api/admin/audit/access', methods=['GET'])
|
||||
@admin_required
|
||||
def list_access_logs():
|
||||
"""List access logs with pagination and filters."""
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = min(request.args.get('per_page', 50, type=int), 200)
|
||||
user_id = request.args.get('user_id', type=int)
|
||||
resource_type = request.args.get('resource_type')
|
||||
resource_id = request.args.get('resource_id', type=int)
|
||||
action = request.args.get('action')
|
||||
|
||||
result = get_access_logs(
|
||||
page=page, per_page=per_page,
|
||||
user_id=user_id, resource_type=resource_type,
|
||||
resource_id=resource_id, action=action,
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
'logs': [log.to_dict() for log in result.items],
|
||||
'total': result.total,
|
||||
'page': result.page,
|
||||
'per_page': per_page,
|
||||
'pages': result.pages,
|
||||
})
|
||||
|
||||
|
||||
@audit_bp.route('/api/admin/audit/auth', methods=['GET'])
|
||||
@admin_required
|
||||
def list_auth_logs():
|
||||
"""List authentication logs with pagination and filters."""
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = min(request.args.get('per_page', 50, type=int), 200)
|
||||
user_id = request.args.get('user_id', type=int)
|
||||
action = request.args.get('action')
|
||||
|
||||
result = get_auth_logs(page=page, per_page=per_page, user_id=user_id, action=action)
|
||||
|
||||
return jsonify({
|
||||
'logs': [log.to_dict() for log in result.items],
|
||||
'total': result.total,
|
||||
'page': result.page,
|
||||
'per_page': per_page,
|
||||
'pages': result.pages,
|
||||
})
|
||||
|
||||
|
||||
@audit_bp.route('/api/admin/audit/recording/<int:recording_id>', methods=['GET'])
|
||||
@admin_required
|
||||
def recording_access_logs(recording_id):
|
||||
"""Get all access logs for a specific recording."""
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = min(request.args.get('per_page', 50, type=int), 200)
|
||||
|
||||
result = get_access_logs(page=page, per_page=per_page, resource_type='recording', resource_id=recording_id)
|
||||
|
||||
return jsonify({
|
||||
'logs': [log.to_dict() for log in result.items],
|
||||
'total': result.total,
|
||||
'page': result.page,
|
||||
'per_page': per_page,
|
||||
'pages': result.pages,
|
||||
})
|
||||
|
||||
|
||||
@audit_bp.route('/api/admin/audit/user/<int:user_id>', methods=['GET'])
|
||||
@admin_required
|
||||
def user_audit_logs(user_id):
|
||||
"""Get all audit logs (access + auth) for a specific user."""
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = min(request.args.get('per_page', 50, type=int), 200)
|
||||
|
||||
access_result = get_access_logs(page=page, per_page=per_page, user_id=user_id)
|
||||
auth_result = get_auth_logs(page=page, per_page=per_page, user_id=user_id)
|
||||
|
||||
return jsonify({
|
||||
'access_logs': {
|
||||
'logs': [log.to_dict() for log in access_result.items],
|
||||
'total': access_result.total,
|
||||
},
|
||||
'auth_logs': {
|
||||
'logs': [log.to_dict() for log in auth_result.items],
|
||||
'total': auth_result.total,
|
||||
},
|
||||
'page': page,
|
||||
'per_page': per_page,
|
||||
})
|
||||
886
src/api/auth.py
Normal file
886
src/api/auth.py
Normal file
@@ -0,0 +1,886 @@
|
||||
"""
|
||||
Authentication and user management routes.
|
||||
|
||||
This blueprint handles user registration, login, logout, account management,
|
||||
and password changes.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import hashlib
|
||||
import mimetypes
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify, session, current_app
|
||||
from flask_login import login_user, logout_user, login_required, current_user
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, PasswordField, SubmitField, BooleanField
|
||||
from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from urllib.parse import urlparse, urljoin
|
||||
import markdown
|
||||
|
||||
from src.database import db
|
||||
from src.models import User, SystemSetting, GroupMembership
|
||||
from src.utils import password_check
|
||||
from src.auth.sso import (
|
||||
init_sso_client,
|
||||
is_sso_enabled,
|
||||
get_sso_config,
|
||||
get_sso_client,
|
||||
create_or_update_sso_user,
|
||||
is_domain_allowed,
|
||||
link_sso_to_existing_user,
|
||||
update_user_profile_from_claims,
|
||||
)
|
||||
from src.services.audit import (
|
||||
audit_login, audit_logout, audit_failed_login,
|
||||
audit_register, audit_password_change, audit_password_reset, audit_sso_login,
|
||||
)
|
||||
from src.services.email import (
|
||||
is_email_verification_enabled,
|
||||
is_email_verification_required,
|
||||
is_smtp_configured,
|
||||
send_verification_email,
|
||||
send_password_reset_email,
|
||||
verify_email_token,
|
||||
verify_reset_token,
|
||||
can_resend_verification,
|
||||
can_resend_password_reset,
|
||||
)
|
||||
|
||||
# Create blueprint
|
||||
auth_bp = Blueprint('auth', __name__)
|
||||
|
||||
# Import these from app after initialization
|
||||
bcrypt = None
|
||||
csrf = None
|
||||
limiter = None
|
||||
|
||||
def init_auth_extensions(_bcrypt, _csrf, _limiter):
|
||||
"""Initialize extensions after app creation."""
|
||||
global bcrypt, csrf, limiter
|
||||
bcrypt = _bcrypt
|
||||
csrf = _csrf
|
||||
limiter = _limiter
|
||||
|
||||
|
||||
def rate_limit(limit_string):
|
||||
"""Decorator that applies rate limiting if limiter is available."""
|
||||
def decorator(f):
|
||||
from functools import wraps
|
||||
@wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
return f(*args, **kwargs)
|
||||
# Store the limit string for later application
|
||||
wrapper._rate_limit = limit_string
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
def csrf_exempt(f):
|
||||
"""Decorator placeholder for CSRF exemption - applied after initialization."""
|
||||
from functools import wraps
|
||||
@wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
return f(*args, **kwargs)
|
||||
wrapper._csrf_exempt = True
|
||||
return wrapper
|
||||
|
||||
|
||||
# --- Forms ---
|
||||
|
||||
class RegistrationForm(FlaskForm):
|
||||
username = StringField('Username', validators=[DataRequired(), Length(min=2, max=20)])
|
||||
email = StringField('Email', validators=[DataRequired(), Email()])
|
||||
password = PasswordField('Password', validators=[DataRequired(), password_check])
|
||||
confirm_password = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')])
|
||||
submit = SubmitField('Sign Up')
|
||||
|
||||
def validate_username(self, username):
|
||||
user = User.query.filter_by(username=username.data).first()
|
||||
if user:
|
||||
raise ValidationError('That username is already taken. Please choose a different one.')
|
||||
|
||||
def validate_email(self, email):
|
||||
user = User.query.filter_by(email=email.data).first()
|
||||
if user:
|
||||
raise ValidationError('That email is already registered. Please use a different one.')
|
||||
|
||||
|
||||
class LoginForm(FlaskForm):
|
||||
email = StringField('Email', validators=[DataRequired(), Email()])
|
||||
password = PasswordField('Mot de passe', validators=[DataRequired()])
|
||||
remember = BooleanField('Se souvenir de moi')
|
||||
submit = SubmitField('Se connecter')
|
||||
|
||||
|
||||
# --- Helper Functions ---
|
||||
|
||||
def is_safe_url(target):
|
||||
ref_url = urlparse(request.host_url)
|
||||
test_url = urlparse(urljoin(request.host_url, target))
|
||||
return test_url.scheme in ('http', 'https') and ref_url.netloc == test_url.netloc
|
||||
|
||||
|
||||
def is_registration_domain_allowed(email: str) -> bool:
|
||||
"""Check if email domain is allowed for registration.
|
||||
|
||||
Returns True if no domain restrictions are configured or if the
|
||||
email domain is in the allowed list.
|
||||
"""
|
||||
if not email:
|
||||
return False
|
||||
|
||||
domains_env = os.environ.get('REGISTRATION_ALLOWED_DOMAINS', '')
|
||||
if not domains_env or not domains_env.strip():
|
||||
return True # No restriction configured
|
||||
|
||||
allowed = [d.strip().lower() for d in domains_env.split(',') if d.strip()]
|
||||
if not allowed:
|
||||
return True # Empty after parsing
|
||||
|
||||
parts = email.lower().rsplit('@', 1)
|
||||
if len(parts) != 2:
|
||||
return False # Invalid email format
|
||||
|
||||
domain = parts[1]
|
||||
return domain in allowed
|
||||
|
||||
|
||||
# --- Routes ---
|
||||
|
||||
@auth_bp.route('/register', methods=['GET', 'POST'])
|
||||
@rate_limit("10 per minute")
|
||||
def register():
|
||||
# Check if registration is allowed
|
||||
allow_registration = os.environ.get('ALLOW_REGISTRATION', 'true').lower() == 'true'
|
||||
|
||||
if not allow_registration:
|
||||
flash('Registration is currently disabled. Please contact the administrator.', 'danger')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('recordings.index'))
|
||||
|
||||
form = RegistrationForm()
|
||||
if form.validate_on_submit():
|
||||
# Check if email domain is allowed
|
||||
if not is_registration_domain_allowed(form.email.data):
|
||||
flash('Registration is restricted. Please contact the administrator.', 'danger')
|
||||
return render_template('register.html', title='Register', form=form)
|
||||
|
||||
hashed_password = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
|
||||
|
||||
# Set email_verified based on whether verification is enabled
|
||||
# If verification is enabled, new users start unverified
|
||||
# If disabled, new users are considered verified by default
|
||||
email_verified = not is_email_verification_enabled()
|
||||
|
||||
user = User(
|
||||
username=form.username.data,
|
||||
email=form.email.data,
|
||||
password=hashed_password,
|
||||
email_verified=email_verified
|
||||
)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
audit_register(user.id)
|
||||
|
||||
# Send verification email if enabled
|
||||
if is_email_verification_enabled() and is_smtp_configured():
|
||||
if send_verification_email(user):
|
||||
return render_template('auth/check_email.html',
|
||||
title='Check Your Email',
|
||||
email=user.email,
|
||||
action='verification')
|
||||
else:
|
||||
# Email failed to send, but account was created
|
||||
flash('Your account has been created, but we could not send a verification email. Please contact support.', 'warning')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
flash('Your account has been created! You can now log in.', 'success')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
return render_template('register.html', title='Register', form=form)
|
||||
|
||||
|
||||
@auth_bp.route('/login', methods=['GET', 'POST'])
|
||||
@rate_limit("10 per minute")
|
||||
def login():
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('recordings.index'))
|
||||
|
||||
sso_enabled = is_sso_enabled()
|
||||
sso_config = get_sso_config()
|
||||
if sso_enabled:
|
||||
init_sso_client(current_app)
|
||||
|
||||
password_login_disabled = sso_enabled and sso_config.get('disable_password_login', False)
|
||||
|
||||
form = LoginForm()
|
||||
if form.validate_on_submit():
|
||||
user = User.query.filter_by(email=form.email.data).first()
|
||||
if user and user.password:
|
||||
# Check if password login is disabled for non-admins
|
||||
if password_login_disabled and not user.is_admin:
|
||||
flash('Password login is disabled. Please sign in with SSO.', 'warning')
|
||||
elif bcrypt.check_password_hash(user.password, form.password.data):
|
||||
# Check email verification if required
|
||||
if is_email_verification_required() and not user.email_verified:
|
||||
# Store user email in session for resend functionality
|
||||
session['unverified_email'] = user.email
|
||||
return render_template('auth/check_email.html',
|
||||
title='Email Verification Required',
|
||||
email=user.email,
|
||||
action='verification_required',
|
||||
show_resend=True)
|
||||
|
||||
login_user(user, remember=form.remember.data)
|
||||
audit_login(user.id)
|
||||
next_page = request.args.get('next')
|
||||
if not is_safe_url(next_page):
|
||||
return redirect(url_for('recordings.index'))
|
||||
return redirect(next_page) if next_page else redirect(url_for('recordings.index'))
|
||||
else:
|
||||
_email_hash = hashlib.sha256(form.email.data.lower().encode()).hexdigest()[:16]
|
||||
audit_failed_login(details={'email_hash': _email_hash, 'reason': 'wrong_password'})
|
||||
flash('Login unsuccessful. Please check email and password.', 'danger')
|
||||
elif user and not user.password:
|
||||
_email_hash = hashlib.sha256(form.email.data.lower().encode()).hexdigest()[:16]
|
||||
audit_failed_login(details={'email_hash': _email_hash, 'reason': 'sso_only_account'})
|
||||
flash('This account uses SSO login. Please sign in with SSO.', 'warning')
|
||||
else:
|
||||
_email_hash = hashlib.sha256(form.email.data.lower().encode()).hexdigest()[:16]
|
||||
audit_failed_login(details={'email_hash': _email_hash, 'reason': 'user_not_found'})
|
||||
flash('Login unsuccessful. Please check email and password.', 'danger')
|
||||
|
||||
return render_template(
|
||||
'login.html',
|
||||
title='Login',
|
||||
form=form,
|
||||
sso_enabled=sso_enabled,
|
||||
sso_provider_name=sso_config.get('provider_name', 'SSO'),
|
||||
password_login_disabled=password_login_disabled
|
||||
)
|
||||
|
||||
|
||||
@auth_bp.route('/auth/sso/login')
|
||||
@rate_limit("10 per minute")
|
||||
def sso_login():
|
||||
if not is_sso_enabled():
|
||||
flash('SSO is not configured. Please contact the administrator.', 'danger')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
oauth = get_sso_client() or init_sso_client(current_app)
|
||||
if not oauth:
|
||||
flash('Failed to initialize SSO client. Check server logs.', 'danger')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
next_url = request.args.get('next')
|
||||
if next_url and is_safe_url(next_url):
|
||||
session['sso_next'] = next_url
|
||||
else:
|
||||
session.pop('sso_next', None)
|
||||
|
||||
try:
|
||||
return oauth.sso.authorize_redirect(redirect_uri=get_sso_config().get('redirect_uri'))
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"SSO redirect failed: {e}")
|
||||
flash('Impossible de joindre le fournisseur SSO. Vérifiez la connexion réseau du serveur.', 'danger')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
|
||||
@auth_bp.route('/auth/sso/callback')
|
||||
@rate_limit("20 per minute")
|
||||
def sso_callback():
|
||||
if not is_sso_enabled():
|
||||
flash('SSO is not configured. Please contact the administrator.', 'danger')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
oauth = get_sso_client() or init_sso_client(current_app)
|
||||
if not oauth:
|
||||
flash('Failed to initialize SSO client. Check server logs.', 'danger')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
try:
|
||||
token = oauth.sso.authorize_access_token()
|
||||
userinfo = token.get('userinfo') or oauth.sso.userinfo()
|
||||
except Exception as e:
|
||||
current_app.logger.warning(f"SSO callback error: {e}")
|
||||
flash('SSO login failed. Please try again.', 'danger')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
subject = userinfo.get('sub')
|
||||
if not subject:
|
||||
flash('SSO response did not include a subject identifier.', 'danger')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
link_user_id = session.pop('sso_link_user_id', None)
|
||||
next_url = session.pop('sso_next', None)
|
||||
cfg = get_sso_config()
|
||||
|
||||
if link_user_id:
|
||||
target_user = db.session.get(User, int(link_user_id))
|
||||
if not target_user:
|
||||
flash('Could not link account: user not found.', 'danger')
|
||||
return redirect(url_for('auth.account'))
|
||||
|
||||
existing = User.query.filter_by(sso_subject=subject).first()
|
||||
if existing and existing.id != target_user.id:
|
||||
flash('This SSO account is already linked to another user.', 'danger')
|
||||
return redirect(url_for('auth.account'))
|
||||
|
||||
update_user_profile_from_claims(target_user, userinfo)
|
||||
target_user.sso_provider = cfg.get('provider_name', 'SSO')
|
||||
target_user.sso_subject = subject
|
||||
db.session.commit()
|
||||
flash('SSO account linked successfully.', 'success')
|
||||
return redirect(url_for('auth.account'))
|
||||
|
||||
try:
|
||||
user = create_or_update_sso_user(userinfo)
|
||||
except PermissionError as e:
|
||||
flash(str(e), 'danger')
|
||||
return redirect(url_for('auth.login'))
|
||||
except ValueError as e:
|
||||
flash(str(e), 'danger')
|
||||
return redirect(url_for('auth.login'))
|
||||
except Exception as e:
|
||||
current_app.logger.warning(f"SSO login error: {e}")
|
||||
flash('Could not complete SSO login. Please try again.', 'danger')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
login_user(user, remember=True)
|
||||
audit_sso_login(user.id, details={'provider': cfg.get('provider_name', 'SSO')})
|
||||
|
||||
if next_url and is_safe_url(next_url):
|
||||
return redirect(next_url)
|
||||
return redirect(url_for('recordings.index'))
|
||||
|
||||
|
||||
@auth_bp.route('/auth/sso/link', methods=['POST'])
|
||||
@login_required
|
||||
def sso_link():
|
||||
if not is_sso_enabled():
|
||||
flash('SSO is not configured. Please contact the administrator.', 'danger')
|
||||
return redirect(url_for('auth.account'))
|
||||
|
||||
session['sso_link_user_id'] = current_user.id
|
||||
session['sso_next'] = url_for('auth.account')
|
||||
|
||||
return redirect(url_for('auth.sso_login'))
|
||||
|
||||
|
||||
@auth_bp.route('/auth/sso/unlink', methods=['POST'])
|
||||
@login_required
|
||||
def sso_unlink():
|
||||
if not current_user.sso_subject:
|
||||
flash('Your account is not linked to SSO.', 'warning')
|
||||
return redirect(url_for('auth.account'))
|
||||
|
||||
if not current_user.password:
|
||||
flash('Cannot unlink SSO - you have no password set. Please set a password first.', 'danger')
|
||||
return redirect(url_for('auth.account'))
|
||||
|
||||
current_user.sso_provider = None
|
||||
current_user.sso_subject = None
|
||||
db.session.commit()
|
||||
flash('SSO account unlinked successfully.', 'success')
|
||||
return redirect(url_for('auth.account'))
|
||||
|
||||
|
||||
@auth_bp.route('/logout')
|
||||
@csrf_exempt
|
||||
def logout():
|
||||
if current_user.is_authenticated:
|
||||
audit_logout(current_user.id)
|
||||
logout_user()
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
|
||||
# --- Email Verification Routes ---
|
||||
|
||||
@auth_bp.route('/verify-email/<token>')
|
||||
def verify_email(token):
|
||||
"""Verify email address using token from email link."""
|
||||
user_id = verify_email_token(token)
|
||||
|
||||
if user_id is None:
|
||||
flash('The verification link is invalid or has expired.', 'danger')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
user = db.session.get(User, user_id)
|
||||
if not user:
|
||||
flash('User not found.', 'danger')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
if user.email_verified:
|
||||
flash('Your email has already been verified.', 'info')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
# Verify the email
|
||||
user.email_verified = True
|
||||
user.email_verification_token = None # Clear the token
|
||||
db.session.commit()
|
||||
|
||||
return render_template('auth/verify_success.html', title='Email Verified')
|
||||
|
||||
|
||||
@auth_bp.route('/resend-verification', methods=['POST'])
|
||||
@rate_limit("3 per minute")
|
||||
def resend_verification():
|
||||
"""Resend verification email."""
|
||||
if not is_email_verification_enabled():
|
||||
flash('Email verification is not enabled.', 'danger')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
if not is_smtp_configured():
|
||||
flash('Email service is not configured.', 'danger')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
# Get email from session (set during failed login) or form
|
||||
email = session.get('unverified_email') or request.form.get('email')
|
||||
|
||||
if not email:
|
||||
flash('Email address is required.', 'danger')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
user = User.query.filter_by(email=email).first()
|
||||
|
||||
if not user:
|
||||
# Don't reveal if user exists
|
||||
flash('If an account exists with this email, a verification link has been sent.', 'info')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
if user.email_verified:
|
||||
flash('Your email has already been verified.', 'info')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
# Check cooldown
|
||||
can_resend, remaining = can_resend_verification(user)
|
||||
if not can_resend:
|
||||
flash(f'Please wait {remaining} seconds before requesting another verification email.', 'warning')
|
||||
return render_template('auth/check_email.html',
|
||||
title='Check Your Email',
|
||||
email=email,
|
||||
action='verification_required',
|
||||
show_resend=True)
|
||||
|
||||
if send_verification_email(user):
|
||||
flash('A new verification email has been sent.', 'success')
|
||||
else:
|
||||
flash('Failed to send verification email. Please try again later.', 'danger')
|
||||
|
||||
return render_template('auth/check_email.html',
|
||||
title='Check Your Email',
|
||||
email=email,
|
||||
action='verification',
|
||||
show_resend=True)
|
||||
|
||||
|
||||
# --- Password Reset Routes ---
|
||||
|
||||
@auth_bp.route('/forgot-password', methods=['GET', 'POST'])
|
||||
@rate_limit("5 per minute")
|
||||
def forgot_password():
|
||||
"""Show and handle forgot password form."""
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('recordings.index'))
|
||||
|
||||
if not is_smtp_configured():
|
||||
flash('Password reset is not available. Please contact the administrator.', 'warning')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
if request.method == 'POST':
|
||||
email = request.form.get('email')
|
||||
|
||||
if not email:
|
||||
flash('Email address is required.', 'danger')
|
||||
return render_template('auth/forgot_password.html', title='Forgot Password')
|
||||
|
||||
user = User.query.filter_by(email=email).first()
|
||||
|
||||
# Always show the same message to prevent email enumeration
|
||||
if user:
|
||||
# Check if user has a password (not SSO-only)
|
||||
if user.password:
|
||||
# Check cooldown
|
||||
can_resend, remaining = can_resend_password_reset(user)
|
||||
if not can_resend:
|
||||
flash(f'Please wait {remaining} seconds before requesting another reset email.', 'warning')
|
||||
else:
|
||||
send_password_reset_email(user)
|
||||
|
||||
flash('If an account exists with this email, a password reset link has been sent.', 'info')
|
||||
return render_template('auth/check_email.html',
|
||||
title='Check Your Email',
|
||||
email=email,
|
||||
action='password_reset')
|
||||
|
||||
return render_template('auth/forgot_password.html', title='Forgot Password')
|
||||
|
||||
|
||||
@auth_bp.route('/reset-password/<token>', methods=['GET', 'POST'])
|
||||
@rate_limit("10 per minute")
|
||||
def reset_password(token):
|
||||
"""Handle password reset form."""
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('recordings.index'))
|
||||
|
||||
user_id = verify_reset_token(token)
|
||||
|
||||
if user_id is None:
|
||||
flash('The password reset link is invalid or has expired.', 'danger')
|
||||
return redirect(url_for('auth.forgot_password'))
|
||||
|
||||
user = db.session.get(User, user_id)
|
||||
if not user:
|
||||
flash('User not found.', 'danger')
|
||||
return redirect(url_for('auth.forgot_password'))
|
||||
|
||||
if request.method == 'POST':
|
||||
password = request.form.get('password')
|
||||
confirm_password = request.form.get('confirm_password')
|
||||
|
||||
if not password or not confirm_password:
|
||||
flash('Both password fields are required.', 'danger')
|
||||
return render_template('auth/reset_password.html', title='Reset Password', token=token)
|
||||
|
||||
if password != confirm_password:
|
||||
flash('Passwords do not match.', 'danger')
|
||||
return render_template('auth/reset_password.html', title='Reset Password', token=token)
|
||||
|
||||
# Validate password
|
||||
try:
|
||||
password_check(None, type('obj', (object,), {'data': password}))
|
||||
except ValidationError as e:
|
||||
flash(str(e), 'danger')
|
||||
return render_template('auth/reset_password.html', title='Reset Password', token=token)
|
||||
|
||||
# Update password
|
||||
hashed_password = bcrypt.generate_password_hash(password).decode('utf-8')
|
||||
user.password = hashed_password
|
||||
user.password_reset_token = None # Clear the token
|
||||
user.password_reset_sent_at = None
|
||||
|
||||
# Also verify email if not already verified
|
||||
if not user.email_verified:
|
||||
user.email_verified = True
|
||||
|
||||
db.session.commit()
|
||||
audit_password_reset(user.id)
|
||||
|
||||
flash('Your password has been reset. You can now log in with your new password.', 'success')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
return render_template('auth/reset_password.html', title='Reset Password', token=token)
|
||||
|
||||
|
||||
@auth_bp.route('/account', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def account():
|
||||
# Import here to avoid circular imports
|
||||
from flask import current_app
|
||||
|
||||
if request.method == 'POST':
|
||||
# Only update fields that are present in the form submission
|
||||
# This prevents clearing data when switching between tabs
|
||||
|
||||
# Check if this is the account information form (has user_name field)
|
||||
if 'user_name' in request.form:
|
||||
# Handle personal information updates
|
||||
user_name = request.form.get('user_name')
|
||||
user_job_title = request.form.get('user_job_title')
|
||||
user_company = request.form.get('user_company')
|
||||
ui_lang = request.form.get('ui_language')
|
||||
transcription_lang = request.form.get('transcription_language')
|
||||
output_lang = request.form.get('output_language')
|
||||
|
||||
current_user.name = user_name if user_name else None
|
||||
current_user.job_title = user_job_title if user_job_title else None
|
||||
current_user.company = user_company if user_company else None
|
||||
current_user.ui_language = ui_lang if ui_lang else 'en'
|
||||
current_user.transcription_language = transcription_lang if transcription_lang else None
|
||||
current_user.output_language = output_lang if output_lang else None
|
||||
|
||||
# Check if this is the custom prompts form (has summary_prompt field)
|
||||
elif 'summary_prompt' in request.form:
|
||||
# Handle custom prompt updates
|
||||
summary_prompt_text = request.form.get('summary_prompt')
|
||||
current_user.summary_prompt = summary_prompt_text if summary_prompt_text else None
|
||||
# Handle event extraction setting
|
||||
current_user.extract_events = 'extract_events' in request.form
|
||||
# Handle transcription hints
|
||||
hotwords = request.form.get('transcription_hotwords')
|
||||
current_user.transcription_hotwords = hotwords if hotwords else None
|
||||
initial_prompt = request.form.get('transcription_initial_prompt')
|
||||
current_user.transcription_initial_prompt = initial_prompt if initial_prompt else None
|
||||
|
||||
# Only update diarize if it's not locked by env var
|
||||
if 'ASR_DIARIZE' not in os.environ:
|
||||
current_user.diarize = 'diarize' in request.form
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# Return JSON response for AJAX requests
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest' or request.accept_mimetypes.best == 'application/json':
|
||||
return jsonify({'success': True, 'message': 'Account details updated successfully!'})
|
||||
|
||||
# Regular form submission with redirect
|
||||
flash('Account details updated successfully!', 'success')
|
||||
|
||||
# Preserve the active tab when redirecting
|
||||
if 'summary_prompt' in request.form:
|
||||
return redirect(url_for('auth.account') + '#prompts')
|
||||
else:
|
||||
return redirect(url_for('auth.account'))
|
||||
|
||||
# Get admin default prompt from system settings
|
||||
admin_default_prompt = SystemSetting.get_setting('admin_default_summary_prompt', None)
|
||||
if admin_default_prompt:
|
||||
default_summary_prompt_text = admin_default_prompt
|
||||
else:
|
||||
# Fallback to hardcoded default if admin hasn't set one
|
||||
default_summary_prompt_text = """Generate a comprehensive summary that includes the following sections:
|
||||
- **Key Issues Discussed**: A bulleted list of the main topics
|
||||
- **Key Decisions Made**: A bulleted list of any decisions reached
|
||||
- **Action Items**: A bulleted list of tasks assigned, including who is responsible if mentioned"""
|
||||
|
||||
asr_diarize_locked = 'ASR_DIARIZE' in os.environ
|
||||
ASR_DIARIZE = os.environ.get('ASR_DIARIZE', 'false').lower() == 'true'
|
||||
USE_ASR_ENDPOINT = os.environ.get('USE_ASR_ENDPOINT', 'false').lower() == 'true'
|
||||
USE_NEW_TRANSCRIPTION_ARCHITECTURE = os.environ.get('USE_NEW_TRANSCRIPTION_ARCHITECTURE', 'true').lower() == 'true'
|
||||
ENABLE_AUTO_DELETION = os.environ.get('ENABLE_AUTO_DELETION', 'false').lower() == 'true'
|
||||
ENABLE_INTERNAL_SHARING = os.environ.get('ENABLE_INTERNAL_SHARING', 'false').lower() == 'true'
|
||||
ASR_RETURN_SPEAKER_EMBEDDINGS = os.environ.get('ASR_RETURN_SPEAKER_EMBEDDINGS', 'false').lower() == 'true'
|
||||
ENABLE_AUTO_EXPORT = os.environ.get('ENABLE_AUTO_EXPORT', 'false').lower() == 'true'
|
||||
|
||||
# Get connector diarization support (new architecture)
|
||||
connector_supports_diarization = USE_ASR_ENDPOINT # Default to USE_ASR_ENDPOINT for backwards compat
|
||||
if USE_NEW_TRANSCRIPTION_ARCHITECTURE:
|
||||
try:
|
||||
from src.services.transcription import get_registry
|
||||
registry = get_registry()
|
||||
connector = registry.get_active_connector()
|
||||
if connector:
|
||||
connector_supports_diarization = connector.supports_diarization
|
||||
except Exception as e:
|
||||
current_app.logger.warning(f"Could not get connector diarization support: {e}")
|
||||
|
||||
# Check if user is a team admin and get their admin groups
|
||||
admin_memberships = GroupMembership.query.filter_by(
|
||||
user_id=current_user.id,
|
||||
role='admin'
|
||||
).all()
|
||||
|
||||
is_team_admin = len(admin_memberships) > 0
|
||||
|
||||
# Build list of groups where user is admin (for tag assignment)
|
||||
user_admin_groups = []
|
||||
for membership in admin_memberships:
|
||||
if membership.group:
|
||||
user_admin_groups.append({
|
||||
'id': membership.group.id,
|
||||
'name': membership.group.name
|
||||
})
|
||||
|
||||
sso_config = get_sso_config()
|
||||
sso_enabled = is_sso_enabled()
|
||||
if sso_enabled:
|
||||
init_sso_client(current_app)
|
||||
sso_linked = bool(current_user.sso_subject)
|
||||
|
||||
password_login_disabled = sso_enabled and sso_config.get('disable_password_login', False)
|
||||
|
||||
# Check if admin has globally disabled auto-summarization
|
||||
admin_setting = SystemSetting.get_setting('disable_auto_summarization', False)
|
||||
admin_disabled_auto_summarization = admin_setting if isinstance(admin_setting, bool) else str(admin_setting).lower() == 'true'
|
||||
|
||||
# Get user's UI language preference
|
||||
user_language = current_user.ui_language if current_user.ui_language else 'en'
|
||||
|
||||
return render_template('account.html',
|
||||
title='Account',
|
||||
default_summary_prompt_text=default_summary_prompt_text,
|
||||
use_asr_endpoint=USE_ASR_ENDPOINT,
|
||||
connector_supports_diarization=connector_supports_diarization,
|
||||
enable_auto_deletion=ENABLE_AUTO_DELETION,
|
||||
enable_internal_sharing=ENABLE_INTERNAL_SHARING,
|
||||
user_admin_groups=user_admin_groups,
|
||||
asr_diarize_locked=asr_diarize_locked,
|
||||
asr_diarize_env_value=ASR_DIARIZE,
|
||||
is_team_admin=is_team_admin,
|
||||
sso_enabled=sso_enabled,
|
||||
sso_provider_name=sso_config.get('provider_name', 'SSO'),
|
||||
sso_linked=sso_linked,
|
||||
sso_subject=current_user.sso_subject,
|
||||
has_password=bool(current_user.password),
|
||||
password_login_disabled=password_login_disabled,
|
||||
speaker_embeddings_enabled=ASR_RETURN_SPEAKER_EMBEDDINGS,
|
||||
auto_speaker_labelling=current_user.auto_speaker_labelling,
|
||||
auto_speaker_labelling_threshold=current_user.auto_speaker_labelling_threshold or 'medium',
|
||||
admin_disabled_auto_summarization=admin_disabled_auto_summarization,
|
||||
auto_summarization=current_user.auto_summarization if current_user.auto_summarization is not None else True,
|
||||
user_language=user_language,
|
||||
enable_auto_export=ENABLE_AUTO_EXPORT)
|
||||
|
||||
|
||||
@auth_bp.route('/api/user/auto-speaker-labelling', methods=['POST'])
|
||||
@login_required
|
||||
def update_auto_speaker_labelling():
|
||||
"""Update user's auto speaker labelling settings."""
|
||||
data = request.get_json()
|
||||
|
||||
if data is None:
|
||||
return jsonify({'success': False, 'error': 'Invalid JSON'}), 400
|
||||
|
||||
# Update enabled state
|
||||
if 'enabled' in data:
|
||||
current_user.auto_speaker_labelling = bool(data['enabled'])
|
||||
|
||||
# Update threshold (validate value)
|
||||
if 'threshold' in data:
|
||||
threshold = data['threshold']
|
||||
if threshold in ('low', 'medium', 'high'):
|
||||
current_user.auto_speaker_labelling_threshold = threshold
|
||||
else:
|
||||
return jsonify({'success': False, 'error': 'Invalid threshold value'}), 400
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'auto_speaker_labelling': current_user.auto_speaker_labelling,
|
||||
'auto_speaker_labelling_threshold': current_user.auto_speaker_labelling_threshold
|
||||
})
|
||||
|
||||
|
||||
@auth_bp.route('/api/user/auto-summarization', methods=['POST'])
|
||||
@login_required
|
||||
def update_auto_summarization():
|
||||
"""Update user's auto summarization setting."""
|
||||
data = request.get_json()
|
||||
|
||||
if data is None:
|
||||
return jsonify({'success': False, 'error': 'Invalid JSON'}), 400
|
||||
|
||||
if 'enabled' in data:
|
||||
current_user.auto_summarization = bool(data['enabled'])
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'auto_summarization': current_user.auto_summarization
|
||||
})
|
||||
|
||||
|
||||
@auth_bp.route('/change_password', methods=['POST'])
|
||||
@login_required
|
||||
@rate_limit("10 per minute")
|
||||
def change_password():
|
||||
# Check if password management is disabled for non-admins
|
||||
sso_config = get_sso_config()
|
||||
password_login_disabled = is_sso_enabled() and sso_config.get('disable_password_login', False)
|
||||
if password_login_disabled and not current_user.is_admin:
|
||||
flash('Password management is disabled. Please use SSO to sign in.', 'warning')
|
||||
return redirect(url_for('auth.account'))
|
||||
|
||||
current_password = request.form.get('current_password')
|
||||
new_password = request.form.get('new_password')
|
||||
confirm_password = request.form.get('confirm_password')
|
||||
|
||||
# Check if user has an existing password
|
||||
has_existing_password = bool(current_user.password)
|
||||
|
||||
# Validate form data - current password only required if user has one
|
||||
if has_existing_password and not current_password:
|
||||
flash('Current password is required.', 'danger')
|
||||
return redirect(url_for('auth.account'))
|
||||
|
||||
if not new_password or not confirm_password:
|
||||
flash('New password and confirmation are required.', 'danger')
|
||||
return redirect(url_for('auth.account'))
|
||||
|
||||
if new_password != confirm_password:
|
||||
flash('New password and confirmation do not match.', 'danger')
|
||||
return redirect(url_for('auth.account'))
|
||||
|
||||
# Custom validation for new password
|
||||
try:
|
||||
password_check(None, type('obj', (object,), {'data': new_password}))
|
||||
except ValidationError as e:
|
||||
flash(str(e), 'danger')
|
||||
return redirect(url_for('auth.account'))
|
||||
|
||||
# Verify current password only if user has one
|
||||
if has_existing_password:
|
||||
if not bcrypt.check_password_hash(current_user.password, current_password):
|
||||
flash('Current password is incorrect.', 'danger')
|
||||
return redirect(url_for('auth.account'))
|
||||
|
||||
# Update password
|
||||
hashed_password = bcrypt.generate_password_hash(new_password).decode('utf-8')
|
||||
current_user.password = hashed_password
|
||||
db.session.commit()
|
||||
audit_password_change(current_user.id)
|
||||
|
||||
flash('Your password has been updated!', 'success')
|
||||
return redirect(url_for('auth.account'))
|
||||
|
||||
|
||||
@auth_bp.route('/docs/transcript-templates-guide')
|
||||
def transcript_templates_guide():
|
||||
"""Serve the transcript templates documentation."""
|
||||
from flask import current_app
|
||||
|
||||
docs_path = os.path.join(current_app.root_path, '..', 'docs', 'transcript-templates-guide.md')
|
||||
|
||||
if not os.path.exists(docs_path):
|
||||
return "Documentation not found", 404
|
||||
|
||||
with open(docs_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Convert markdown to HTML
|
||||
html_content = markdown.markdown(content, extensions=['tables', 'fenced_code', 'codehilite'])
|
||||
|
||||
# Wrap in basic HTML template with Speakr styling
|
||||
html_template = f'''
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Transcript Templates Guide - Speakr</title>
|
||||
<link rel="stylesheet" href="/static/css/output.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||
<style>
|
||||
.markdown-body {{
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
line-height: 1.6;
|
||||
}}
|
||||
.markdown-body h1 {{ font-size: 2.5rem; margin-bottom: 1rem; }}
|
||||
.markdown-body h2 {{ font-size: 2rem; margin-top: 2rem; margin-bottom: 1rem; }}
|
||||
.markdown-body h3 {{ font-size: 1.5rem; margin-top: 1.5rem; margin-bottom: 0.75rem; }}
|
||||
.markdown-body pre {{ background: #f4f4f4; padding: 1rem; border-radius: 0.5rem; overflow-x: auto; }}
|
||||
.markdown-body code {{ background: #f4f4f4; padding: 0.2rem 0.4rem; border-radius: 0.25rem; }}
|
||||
.markdown-body pre code {{ background: none; padding: 0; }}
|
||||
.markdown-body ul, .markdown-body ol {{ margin-left: 2rem; margin-bottom: 1rem; }}
|
||||
.markdown-body li {{ margin-bottom: 0.5rem; }}
|
||||
.markdown-body blockquote {{ border-left: 4px solid #ddd; padding-left: 1rem; margin: 1rem 0; }}
|
||||
.markdown-body table {{ border-collapse: collapse; width: 100%; margin: 1rem 0; }}
|
||||
.markdown-body th, .markdown-body td {{ border: 1px solid #ddd; padding: 0.5rem; }}
|
||||
.markdown-body th {{ background: #f4f4f4; font-weight: bold; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="markdown-body">
|
||||
<a href="/" class="btn-primary" style="display: inline-block; margin-bottom: 1rem; padding: 0.5rem 1rem; background: #3b82f6; color: white; text-decoration: none; border-radius: 0.5rem;">← Back to App</a>
|
||||
{html_content}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
return html_template
|
||||
201
src/api/docs.py
Normal file
201
src/api/docs.py
Normal file
@@ -0,0 +1,201 @@
|
||||
"""Documentation API - serves client documentation pages."""
|
||||
import os
|
||||
import markdown
|
||||
from markupsafe import escape
|
||||
from flask import Blueprint, jsonify
|
||||
from flask_login import login_required
|
||||
from src.utils import sanitize_html
|
||||
|
||||
docs_bp = Blueprint('docs', __name__)
|
||||
|
||||
# Path to client documentation files
|
||||
DOCS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), 'client_docs')
|
||||
|
||||
# Singleton Markdown instance (performance: avoid reinitializing extensions on every call)
|
||||
_docs_md = markdown.Markdown(
|
||||
extensions=[
|
||||
'tables',
|
||||
'fenced_code',
|
||||
'toc',
|
||||
'admonition',
|
||||
'attr_list',
|
||||
'md_in_html',
|
||||
'sane_lists'
|
||||
],
|
||||
extension_configs={
|
||||
'toc': {'permalink': False, 'toc_depth': 3}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _read_doc_file(filepath):
|
||||
"""Read and cache a documentation file. Returns content string."""
|
||||
mtime = os.path.getmtime(filepath)
|
||||
cache = getattr(_read_doc_file, '_cache', {})
|
||||
cached = cache.get(filepath)
|
||||
if cached and cached[0] == mtime:
|
||||
return cached[1]
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
cache[filepath] = (mtime, content)
|
||||
_read_doc_file._cache = cache
|
||||
return content
|
||||
|
||||
|
||||
def get_nav_structure(is_admin=False):
|
||||
"""Return navigation structure for documentation."""
|
||||
sections = [
|
||||
{
|
||||
"title": "Guide Utilisateur",
|
||||
"icon": "fa-book-open",
|
||||
"slug": "guide-utilisateur",
|
||||
"pages": [
|
||||
{"title": "Vue d'ensemble", "slug": "index"},
|
||||
{"title": "Premiers pas", "slug": "premiers-pas"},
|
||||
{"title": "Enregistrement", "slug": "enregistrement"},
|
||||
{"title": "Transcriptions", "slug": "transcriptions"},
|
||||
{"title": "Recherche IA", "slug": "recherche-ia"},
|
||||
{"title": "Partage", "slug": "partage"},
|
||||
{"title": "Dossiers", "slug": "dossiers"},
|
||||
{"title": "Groupes", "slug": "groupes"},
|
||||
{"title": "Paramètres", "slug": "parametres"},
|
||||
{"title": "Application mobile", "slug": "application-mobile"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Dépannage",
|
||||
"icon": "fa-life-ring",
|
||||
"slug": "depannage",
|
||||
"pages": [
|
||||
{"title": "Vue d'ensemble", "slug": "index"},
|
||||
{"title": "Transcription", "slug": "transcription"},
|
||||
{"title": "Performance", "slug": "performance"},
|
||||
{"title": "Fonctionnalités", "slug": "fonctionnalites"}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
if is_admin:
|
||||
sections.insert(1, {
|
||||
"title": "Guide Administrateur",
|
||||
"icon": "fa-shield-alt",
|
||||
"slug": "guide-admin",
|
||||
"pages": [
|
||||
{"title": "Vue d'ensemble", "slug": "index"},
|
||||
{"title": "Gestion des utilisateurs", "slug": "gestion-utilisateurs"},
|
||||
{"title": "Gestion des groupes", "slug": "gestion-groupes"},
|
||||
{"title": "Statistiques", "slug": "statistiques"},
|
||||
{"title": "Paramètres système", "slug": "parametres-systeme"},
|
||||
{"title": "Modèles IA", "slug": "modeles-ia"},
|
||||
{"title": "Prompts", "slug": "prompts"},
|
||||
{"title": "Recherche sémantique", "slug": "recherche-semantique"},
|
||||
{"title": "Rétention", "slug": "retention"},
|
||||
{"title": "SSO", "slug": "sso"}
|
||||
]
|
||||
})
|
||||
|
||||
return sections
|
||||
|
||||
|
||||
def render_markdown_content(md_text):
|
||||
"""Render markdown to sanitized HTML with extensions for admonitions, tables, etc."""
|
||||
_docs_md.reset()
|
||||
html = _docs_md.convert(md_text)
|
||||
toc_html = getattr(_docs_md, 'toc', '')
|
||||
# Sanitize rendered HTML to prevent XSS
|
||||
html = sanitize_html(html)
|
||||
return html, toc_html
|
||||
|
||||
|
||||
@docs_bp.route('/api/docs/nav')
|
||||
@login_required
|
||||
def docs_nav():
|
||||
"""Return navigation structure based on user role."""
|
||||
from flask_login import current_user
|
||||
is_admin = getattr(current_user, 'is_admin', False)
|
||||
sections = get_nav_structure(is_admin)
|
||||
return jsonify({"sections": sections})
|
||||
|
||||
|
||||
@docs_bp.route('/api/docs/page/<section>/<page>')
|
||||
@login_required
|
||||
def docs_page(section, page):
|
||||
"""Return rendered HTML for a documentation page."""
|
||||
# Sanitize path components to prevent directory traversal
|
||||
safe_section = os.path.basename(section)
|
||||
safe_page = os.path.basename(page)
|
||||
|
||||
allowed_sections = ['guide-utilisateur', 'guide-admin', 'depannage']
|
||||
if safe_section not in allowed_sections:
|
||||
return jsonify({"error": "Section invalide"}), 404
|
||||
|
||||
# Admin guide requires admin role
|
||||
if safe_section == 'guide-admin':
|
||||
from flask_login import current_user
|
||||
if not getattr(current_user, 'is_admin', False):
|
||||
return jsonify({"error": "Accès refusé"}), 403
|
||||
|
||||
filepath = os.path.join(DOCS_DIR, safe_section, f"{safe_page}.md")
|
||||
|
||||
if not os.path.isfile(filepath):
|
||||
return jsonify({"error": "Page non trouvée"}), 404
|
||||
|
||||
content = _read_doc_file(filepath)
|
||||
html, toc = render_markdown_content(content)
|
||||
|
||||
return jsonify({
|
||||
"html": html,
|
||||
"toc": toc,
|
||||
"section": safe_section,
|
||||
"page": safe_page
|
||||
})
|
||||
|
||||
|
||||
@docs_bp.route('/api/docs/search')
|
||||
@login_required
|
||||
def docs_search():
|
||||
"""Simple text search across all documentation pages."""
|
||||
from flask import request
|
||||
from flask_login import current_user
|
||||
|
||||
query = request.args.get('q', '').strip().lower()
|
||||
if not query or len(query) < 2:
|
||||
return jsonify({"results": []})
|
||||
|
||||
is_admin = getattr(current_user, 'is_admin', False)
|
||||
results = []
|
||||
|
||||
sections = get_nav_structure(is_admin)
|
||||
|
||||
for section in sections:
|
||||
section_dir = os.path.join(DOCS_DIR, section['slug'])
|
||||
if not os.path.isdir(section_dir):
|
||||
continue
|
||||
|
||||
for page_info in section['pages']:
|
||||
filepath = os.path.join(section_dir, f"{page_info['slug']}.md")
|
||||
if not os.path.isfile(filepath):
|
||||
continue
|
||||
|
||||
content = _read_doc_file(filepath)
|
||||
|
||||
content_lower = content.lower()
|
||||
if query in content_lower:
|
||||
# Find matching line for context
|
||||
lines = content.split('\n')
|
||||
snippet = ''
|
||||
for line in lines:
|
||||
if query in line.lower():
|
||||
# HTML-escape snippet to prevent XSS
|
||||
snippet = str(escape(line.strip()[:150]))
|
||||
break
|
||||
|
||||
results.append({
|
||||
"section": section['slug'],
|
||||
"section_title": section['title'],
|
||||
"page": page_info['slug'],
|
||||
"page_title": page_info['title'],
|
||||
"snippet": snippet
|
||||
})
|
||||
|
||||
return jsonify({"results": results})
|
||||
173
src/api/events.py
Normal file
173
src/api/events.py
Normal file
@@ -0,0 +1,173 @@
|
||||
"""
|
||||
Calendar event extraction and export.
|
||||
|
||||
This blueprint was auto-generated from app.py route extraction.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify, send_file, Response, current_app, make_response
|
||||
from flask_login import login_required, current_user
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from src.database import db
|
||||
from src.models import *
|
||||
from src.utils import *
|
||||
from src.services.calendar import generate_ics_content
|
||||
|
||||
# Create blueprint
|
||||
events_bp = Blueprint('events', __name__)
|
||||
|
||||
# Configuration from environment
|
||||
ENABLE_INQUIRE_MODE = os.environ.get('ENABLE_INQUIRE_MODE', 'false').lower() == 'true'
|
||||
ENABLE_AUTO_DELETION = os.environ.get('ENABLE_AUTO_DELETION', 'false').lower() == 'true'
|
||||
USERS_CAN_DELETE = os.environ.get('USERS_CAN_DELETE', 'true').lower() == 'true'
|
||||
ENABLE_INTERNAL_SHARING = os.environ.get('ENABLE_INTERNAL_SHARING', 'false').lower() == 'true'
|
||||
USE_ASR_ENDPOINT = os.environ.get('USE_ASR_ENDPOINT', 'false').lower() == 'true'
|
||||
|
||||
# Global helpers (will be injected from app)
|
||||
has_recording_access = None
|
||||
bcrypt = None
|
||||
csrf = None
|
||||
limiter = None
|
||||
|
||||
def init_events_helpers(**kwargs):
|
||||
"""Initialize helper functions and extensions from app."""
|
||||
global has_recording_access, bcrypt, csrf, limiter
|
||||
has_recording_access = kwargs.get('has_recording_access')
|
||||
bcrypt = kwargs.get('bcrypt')
|
||||
csrf = kwargs.get('csrf')
|
||||
limiter = kwargs.get('limiter')
|
||||
|
||||
|
||||
# --- Routes ---
|
||||
|
||||
@events_bp.route('/api/recording/<int:recording_id>/events', methods=['GET'])
|
||||
@login_required
|
||||
def get_recording_events(recording_id):
|
||||
"""Get all events extracted from a recording."""
|
||||
try:
|
||||
recording = db.session.get(Recording, recording_id)
|
||||
if not recording:
|
||||
return jsonify({'error': 'Recording not found'}), 404
|
||||
|
||||
if not has_recording_access(recording, current_user):
|
||||
return jsonify({'error': 'Unauthorized'}), 403
|
||||
|
||||
events = Event.query.filter_by(recording_id=recording_id).all()
|
||||
return jsonify({'events': [event.to_dict() for event in events]})
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error fetching events for recording {recording_id}: {e}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@events_bp.route('/api/event/<int:event_id>/ics', methods=['GET'])
|
||||
@login_required
|
||||
def download_event_ics(event_id):
|
||||
"""Generate and download an ICS file for a single event."""
|
||||
try:
|
||||
event = db.session.get(Event, event_id)
|
||||
if not event:
|
||||
return jsonify({'error': 'Event not found'}), 404
|
||||
|
||||
# Check permissions through recording access
|
||||
if not has_recording_access(event.recording, current_user):
|
||||
return jsonify({'error': 'Unauthorized'}), 403
|
||||
|
||||
# Generate ICS content
|
||||
ics_content = generate_ics_content(event)
|
||||
|
||||
# Create response with ICS file
|
||||
response = make_response(ics_content)
|
||||
response.headers['Content-Type'] = 'text/calendar; charset=utf-8'
|
||||
response.headers['Content-Disposition'] = f'attachment; filename="{secure_filename(event.title)}.ics"'
|
||||
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error generating ICS for event {event_id}: {e}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@events_bp.route('/api/recording/<int:recording_id>/events/ics', methods=['GET'])
|
||||
@login_required
|
||||
def download_all_events_ics(recording_id):
|
||||
"""Generate and download an ICS file containing all events from a recording."""
|
||||
try:
|
||||
recording = db.session.get(Recording, recording_id)
|
||||
if not recording:
|
||||
return jsonify({'error': 'Recording not found'}), 404
|
||||
|
||||
if not has_recording_access(recording, current_user):
|
||||
return jsonify({'error': 'Unauthorized'}), 403
|
||||
|
||||
# Get all events for this recording
|
||||
events = Event.query.filter_by(recording_id=recording_id).all()
|
||||
if not events:
|
||||
return jsonify({'error': 'No events found for this recording'}), 404
|
||||
|
||||
# Generate combined ICS content
|
||||
ics_lines = []
|
||||
ics_lines.append("BEGIN:VCALENDAR")
|
||||
ics_lines.append("VERSION:2.0")
|
||||
ics_lines.append("PRODID:-//Speakr//Event Export//EN")
|
||||
ics_lines.append("CALSCALE:GREGORIAN")
|
||||
ics_lines.append("METHOD:PUBLISH")
|
||||
|
||||
# Add each event
|
||||
for event in events:
|
||||
# Get the individual event's ICS content and extract just the VEVENT portion
|
||||
individual_ics = generate_ics_content(event)
|
||||
# Extract VEVENT block from individual ICS
|
||||
lines = individual_ics.split('\n')
|
||||
in_event = False
|
||||
for line in lines:
|
||||
if line.startswith('BEGIN:VEVENT'):
|
||||
in_event = True
|
||||
if in_event:
|
||||
ics_lines.append(line)
|
||||
if line.startswith('END:VEVENT'):
|
||||
in_event = False
|
||||
|
||||
ics_lines.append("END:VCALENDAR")
|
||||
ics_content = '\r\n'.join(ics_lines)
|
||||
|
||||
# Create response with ICS file
|
||||
response = make_response(ics_content)
|
||||
response.headers['Content-Type'] = 'text/calendar; charset=utf-8'
|
||||
safe_title = secure_filename(recording.title) if recording.title else f'recording-{recording_id}'
|
||||
response.headers['Content-Disposition'] = f'attachment; filename="{safe_title}-events.ics"'
|
||||
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error generating ICS for all events in recording {recording_id}: {e}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@events_bp.route('/api/event/<int:event_id>', methods=['DELETE'])
|
||||
@login_required
|
||||
def delete_event(event_id):
|
||||
"""Delete a single event."""
|
||||
try:
|
||||
event = db.session.get(Event, event_id)
|
||||
if not event:
|
||||
return jsonify({'error': 'Event not found'}), 404
|
||||
|
||||
# Check permissions through recording access
|
||||
if not has_recording_access(event.recording, current_user):
|
||||
return jsonify({'error': 'Unauthorized'}), 403
|
||||
|
||||
db.session.delete(event)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'success': True})
|
||||
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Error deleting event {event_id}: {e}")
|
||||
db.session.rollback()
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
162
src/api/export_templates.py
Normal file
162
src/api/export_templates.py
Normal file
@@ -0,0 +1,162 @@
|
||||
"""
|
||||
Export template management API.
|
||||
|
||||
This blueprint provides CRUD operations for export templates,
|
||||
following the same pattern as transcript templates.
|
||||
"""
|
||||
|
||||
import os
|
||||
from datetime import datetime
|
||||
from flask import Blueprint, request, jsonify, current_app
|
||||
from flask_login import login_required, current_user
|
||||
|
||||
from src.database import db
|
||||
from src.models import ExportTemplate
|
||||
|
||||
# Create blueprint
|
||||
export_templates_bp = Blueprint('export_templates', __name__)
|
||||
|
||||
# Configuration from environment
|
||||
ENABLE_AUTO_EXPORT = os.environ.get('ENABLE_AUTO_EXPORT', 'false').lower() == 'true'
|
||||
|
||||
|
||||
# --- Routes ---
|
||||
|
||||
@export_templates_bp.route('/api/export-templates', methods=['GET'])
|
||||
@login_required
|
||||
def get_export_templates():
|
||||
"""Get all export templates for the current user."""
|
||||
templates = ExportTemplate.query.filter_by(user_id=current_user.id).all()
|
||||
return jsonify([template.to_dict() for template in templates])
|
||||
|
||||
|
||||
@export_templates_bp.route('/api/export-templates', methods=['POST'])
|
||||
@login_required
|
||||
def create_export_template():
|
||||
"""Create a new export template."""
|
||||
data = request.json
|
||||
if not data or not data.get('name') or not data.get('template'):
|
||||
return jsonify({'error': 'Name and template are required'}), 400
|
||||
|
||||
# If this is set as default, unset other defaults
|
||||
if data.get('is_default'):
|
||||
ExportTemplate.query.filter_by(
|
||||
user_id=current_user.id,
|
||||
is_default=True
|
||||
).update({'is_default': False})
|
||||
|
||||
template = ExportTemplate(
|
||||
user_id=current_user.id,
|
||||
name=data['name'],
|
||||
template=data['template'],
|
||||
description=data.get('description'),
|
||||
is_default=data.get('is_default', False)
|
||||
)
|
||||
|
||||
db.session.add(template)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify(template.to_dict()), 201
|
||||
|
||||
|
||||
@export_templates_bp.route('/api/export-templates/<int:template_id>', methods=['PUT'])
|
||||
@login_required
|
||||
def update_export_template(template_id):
|
||||
"""Update an existing export template."""
|
||||
template = ExportTemplate.query.filter_by(
|
||||
id=template_id,
|
||||
user_id=current_user.id
|
||||
).first()
|
||||
|
||||
if not template:
|
||||
return jsonify({'error': 'Template not found'}), 404
|
||||
|
||||
data = request.json
|
||||
if not data:
|
||||
return jsonify({'error': 'No data provided'}), 400
|
||||
|
||||
# If this is set as default, unset other defaults
|
||||
if data.get('is_default'):
|
||||
ExportTemplate.query.filter_by(
|
||||
user_id=current_user.id,
|
||||
is_default=True
|
||||
).update({'is_default': False})
|
||||
|
||||
template.name = data.get('name', template.name)
|
||||
template.template = data.get('template', template.template)
|
||||
template.description = data.get('description', template.description)
|
||||
template.is_default = data.get('is_default', template.is_default)
|
||||
template.updated_at = datetime.utcnow()
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify(template.to_dict())
|
||||
|
||||
|
||||
@export_templates_bp.route('/api/export-templates/<int:template_id>', methods=['DELETE'])
|
||||
@login_required
|
||||
def delete_export_template(template_id):
|
||||
"""Delete an export template."""
|
||||
template = ExportTemplate.query.filter_by(
|
||||
id=template_id,
|
||||
user_id=current_user.id
|
||||
).first()
|
||||
|
||||
if not template:
|
||||
return jsonify({'error': 'Template not found'}), 404
|
||||
|
||||
db.session.delete(template)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
@export_templates_bp.route('/api/export-templates/create-defaults', methods=['POST'])
|
||||
@login_required
|
||||
def create_default_export_templates():
|
||||
"""Create default export template for the user if they don't have any."""
|
||||
existing_templates = ExportTemplate.query.filter_by(user_id=current_user.id).count()
|
||||
|
||||
if existing_templates > 0:
|
||||
return jsonify({'message': 'User already has templates'}), 200
|
||||
|
||||
# Default template with localized labels
|
||||
default_template = ExportTemplate(
|
||||
user_id=current_user.id,
|
||||
name="Standard Export",
|
||||
template="""# {{title}}
|
||||
|
||||
## {{label.metadata}}
|
||||
|
||||
{{#if meeting_date}}- **{{label.date}}:** {{meeting_date}}
|
||||
{{/if}}{{#if created_at}}- **{{label.created}}:** {{created_at}}
|
||||
{{/if}}{{#if original_filename}}- **{{label.originalFile}}:** {{original_filename}}
|
||||
{{/if}}{{#if file_size}}- **{{label.fileSize}}:** {{file_size}}
|
||||
{{/if}}{{#if participants}}- **{{label.participants}}:** {{participants}}
|
||||
{{/if}}{{#if tags}}- **{{label.tags}}:** {{tags}}
|
||||
{{/if}}
|
||||
|
||||
{{#if notes}}## {{label.notes}}
|
||||
|
||||
{{notes}}
|
||||
|
||||
{{/if}}{{#if summary}}## {{label.summary}}
|
||||
|
||||
{{summary}}
|
||||
|
||||
{{/if}}{{#if transcription}}## {{label.transcription}}
|
||||
|
||||
{{transcription}}
|
||||
|
||||
{{/if}}""",
|
||||
description="Default export template with localized labels",
|
||||
is_default=True
|
||||
)
|
||||
|
||||
db.session.add(default_template)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'templates': [default_template.to_dict()]
|
||||
}), 201
|
||||
665
src/api/folders.py
Normal file
665
src/api/folders.py
Normal file
@@ -0,0 +1,665 @@
|
||||
"""
|
||||
Folder management and assignment.
|
||||
|
||||
This blueprint handles folder CRUD operations and recording-folder assignments.
|
||||
Folders are one-to-many (a recording can only belong to one folder).
|
||||
"""
|
||||
|
||||
import os
|
||||
from datetime import datetime
|
||||
from flask import Blueprint, request, jsonify, current_app
|
||||
from flask_login import login_required, current_user
|
||||
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from src.database import db
|
||||
from src.services.audit import audit_access
|
||||
from src.models import *
|
||||
|
||||
# Create blueprint
|
||||
folders_bp = Blueprint('folders', __name__)
|
||||
|
||||
# Configuration from environment
|
||||
ENABLE_AUTO_DELETION = os.environ.get('ENABLE_AUTO_DELETION', 'false').lower() == 'true'
|
||||
ENABLE_INTERNAL_SHARING = os.environ.get('ENABLE_INTERNAL_SHARING', 'false').lower() == 'true'
|
||||
|
||||
# Global helpers (will be injected from app)
|
||||
has_recording_access = None
|
||||
bcrypt = None
|
||||
csrf = None
|
||||
limiter = None
|
||||
|
||||
|
||||
def init_folders_helpers(**kwargs):
|
||||
"""Initialize helper functions and extensions from app."""
|
||||
global has_recording_access, bcrypt, csrf, limiter
|
||||
has_recording_access = kwargs.get('has_recording_access')
|
||||
bcrypt = kwargs.get('bcrypt')
|
||||
csrf = kwargs.get('csrf')
|
||||
limiter = kwargs.get('limiter')
|
||||
|
||||
|
||||
# --- Routes ---
|
||||
|
||||
@folders_bp.route('/api/folders', methods=['GET'])
|
||||
@login_required
|
||||
def get_folders():
|
||||
"""Get all folders for the current user, including group folders they have access to."""
|
||||
# Check if folders feature is enabled - return empty array if not
|
||||
folders_enabled = SystemSetting.get_setting('enable_folders', False)
|
||||
if not folders_enabled:
|
||||
return jsonify([])
|
||||
|
||||
# Get user's personal folders
|
||||
user_folders = Folder.query.filter_by(user_id=current_user.id, group_id=None).order_by(Folder.name).all()
|
||||
|
||||
# Get user's team memberships with roles
|
||||
memberships = GroupMembership.query.filter_by(user_id=current_user.id).all()
|
||||
team_roles = {m.group_id: m.role for m in memberships}
|
||||
team_ids = list(team_roles.keys())
|
||||
|
||||
# Get group folders for all teams the user is a member of
|
||||
team_folders = []
|
||||
if team_ids:
|
||||
team_folders = Folder.query.filter(Folder.group_id.in_(team_ids)).order_by(Folder.name).all()
|
||||
|
||||
# Build response with edit permissions
|
||||
result = []
|
||||
|
||||
# Personal folders - user can always edit their own
|
||||
for folder in user_folders:
|
||||
folder_dict = folder.to_dict()
|
||||
folder_dict['can_edit'] = True
|
||||
folder_dict['user_role'] = None
|
||||
result.append(folder_dict)
|
||||
|
||||
# Group folders - only admins can edit
|
||||
for folder in team_folders:
|
||||
folder_dict = folder.to_dict()
|
||||
user_role = team_roles.get(folder.group_id, 'member')
|
||||
folder_dict['can_edit'] = (user_role == 'admin')
|
||||
folder_dict['user_role'] = user_role
|
||||
result.append(folder_dict)
|
||||
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@folders_bp.route('/api/folders', methods=['POST'])
|
||||
@login_required
|
||||
def create_folder():
|
||||
"""Create a new folder (personal or group folder)."""
|
||||
# Check if folders feature is enabled
|
||||
folders_enabled = SystemSetting.get_setting('enable_folders', False)
|
||||
if not folders_enabled:
|
||||
return jsonify({'error': 'Folders feature is not enabled'}), 403
|
||||
|
||||
data = request.get_json()
|
||||
|
||||
if not data or not data.get('name'):
|
||||
return jsonify({'error': 'Folder name is required'}), 400
|
||||
|
||||
group_id = data.get('group_id')
|
||||
|
||||
# If creating a group folder, verify user is admin of that group
|
||||
if group_id:
|
||||
membership = GroupMembership.query.filter_by(
|
||||
group_id=group_id,
|
||||
user_id=current_user.id
|
||||
).first()
|
||||
|
||||
if not membership or membership.role != 'admin':
|
||||
return jsonify({'error': 'Only group admins can create group folders'}), 403
|
||||
|
||||
# Check if group folder with same name already exists for this group
|
||||
existing_folder = Folder.query.filter_by(name=data['name'], group_id=group_id).first()
|
||||
if existing_folder:
|
||||
return jsonify({'error': 'A folder with this name already exists for this group'}), 400
|
||||
else:
|
||||
# Check if personal folder with same name already exists for this user
|
||||
existing_folder = Folder.query.filter_by(name=data['name'], user_id=current_user.id, group_id=None).first()
|
||||
if existing_folder:
|
||||
return jsonify({'error': 'Folder with this name already exists'}), 400
|
||||
|
||||
# Handle retention_days: -1 means protected from deletion
|
||||
retention_days = data.get('retention_days')
|
||||
protect_from_deletion = False
|
||||
|
||||
if retention_days == -1:
|
||||
# -1 indicates infinite retention (protected from auto-deletion)
|
||||
protect_from_deletion = True if ENABLE_AUTO_DELETION else False
|
||||
|
||||
# Validate naming_template_id if provided
|
||||
naming_template_id = data.get('naming_template_id')
|
||||
if naming_template_id:
|
||||
template = NamingTemplate.query.filter_by(id=naming_template_id, user_id=current_user.id).first()
|
||||
if not template:
|
||||
return jsonify({'error': 'Naming template not found'}), 404
|
||||
|
||||
# Validate export_template_id if provided
|
||||
export_template_id = data.get('export_template_id')
|
||||
if export_template_id:
|
||||
template = ExportTemplate.query.filter_by(id=export_template_id, user_id=current_user.id).first()
|
||||
if not template:
|
||||
return jsonify({'error': 'Export template not found'}), 404
|
||||
|
||||
folder = Folder(
|
||||
name=data['name'],
|
||||
user_id=current_user.id,
|
||||
group_id=group_id,
|
||||
color=data.get('color', '#10B981'),
|
||||
custom_prompt=data.get('custom_prompt'),
|
||||
default_language=data.get('default_language'),
|
||||
default_min_speakers=data.get('default_min_speakers'),
|
||||
default_max_speakers=data.get('default_max_speakers'),
|
||||
default_hotwords=data.get('default_hotwords'),
|
||||
default_initial_prompt=data.get('default_initial_prompt'),
|
||||
protect_from_deletion=protect_from_deletion,
|
||||
retention_days=retention_days,
|
||||
auto_share_on_apply=data.get('auto_share_on_apply', True) if group_id else True,
|
||||
share_with_group_lead=data.get('share_with_group_lead', True) if group_id else True,
|
||||
naming_template_id=naming_template_id,
|
||||
export_template_id=export_template_id
|
||||
)
|
||||
|
||||
db.session.add(folder)
|
||||
|
||||
try:
|
||||
db.session.commit()
|
||||
except IntegrityError as e:
|
||||
db.session.rollback()
|
||||
current_app.logger.error(f"Folder creation failed due to integrity constraint: {str(e)}")
|
||||
return jsonify({'error': 'A folder with this name already exists'}), 400
|
||||
|
||||
return jsonify(folder.to_dict()), 201
|
||||
|
||||
|
||||
@folders_bp.route('/api/folders/<int:folder_id>', methods=['PUT'])
|
||||
@login_required
|
||||
def update_folder(folder_id):
|
||||
"""Update a folder."""
|
||||
# Check if folders feature is enabled
|
||||
folders_enabled = SystemSetting.get_setting('enable_folders', False)
|
||||
if not folders_enabled:
|
||||
return jsonify({'error': 'Folders feature is not enabled'}), 403
|
||||
|
||||
folder = db.session.get(Folder, folder_id)
|
||||
if not folder:
|
||||
return jsonify({'error': 'Folder not found'}), 404
|
||||
|
||||
# Check permissions
|
||||
if folder.group_id:
|
||||
# Group folder - user must be a team admin
|
||||
membership = GroupMembership.query.filter_by(
|
||||
group_id=folder.group_id,
|
||||
user_id=current_user.id
|
||||
).first()
|
||||
|
||||
if not membership or membership.role != 'admin':
|
||||
return jsonify({'error': 'Only group admins can edit group folders'}), 403
|
||||
else:
|
||||
# Personal folder - must be the owner
|
||||
if folder.user_id != current_user.id:
|
||||
return jsonify({'error': 'You do not have permission to edit this folder'}), 403
|
||||
|
||||
data = request.get_json()
|
||||
|
||||
if 'name' in data:
|
||||
# Check if new name conflicts with another folder
|
||||
if folder.group_id:
|
||||
existing_folder = Folder.query.filter_by(name=data['name'], group_id=folder.group_id).filter(Folder.id != folder_id).first()
|
||||
else:
|
||||
existing_folder = Folder.query.filter_by(name=data['name'], user_id=current_user.id).filter(Folder.id != folder_id).first()
|
||||
|
||||
if existing_folder:
|
||||
return jsonify({'error': 'Another folder with this name already exists'}), 400
|
||||
folder.name = data['name']
|
||||
|
||||
# Handle group_id changes (converting between personal and group folders)
|
||||
if 'group_id' in data:
|
||||
new_group_id = data['group_id'] if data['group_id'] else None
|
||||
|
||||
# If changing to a group folder, verify user is admin of that group
|
||||
if new_group_id:
|
||||
membership = GroupMembership.query.filter_by(
|
||||
group_id=new_group_id,
|
||||
user_id=current_user.id
|
||||
).first()
|
||||
|
||||
if not membership or membership.role != 'admin':
|
||||
return jsonify({'error': 'Only group admins can assign folders to groups'}), 403
|
||||
|
||||
folder.group_id = new_group_id
|
||||
|
||||
if 'color' in data:
|
||||
folder.color = data['color']
|
||||
if 'custom_prompt' in data:
|
||||
folder.custom_prompt = data['custom_prompt']
|
||||
if 'default_language' in data:
|
||||
folder.default_language = data['default_language']
|
||||
if 'default_min_speakers' in data:
|
||||
folder.default_min_speakers = data['default_min_speakers']
|
||||
if 'default_max_speakers' in data:
|
||||
folder.default_max_speakers = data['default_max_speakers']
|
||||
if 'default_hotwords' in data:
|
||||
folder.default_hotwords = data['default_hotwords'] or None
|
||||
if 'default_initial_prompt' in data:
|
||||
folder.default_initial_prompt = data['default_initial_prompt'] or None
|
||||
|
||||
# Handle retention_days: -1 means protected from deletion
|
||||
if 'retention_days' in data:
|
||||
retention_days = data['retention_days']
|
||||
|
||||
if retention_days == -1:
|
||||
# -1 indicates infinite retention (protected from auto-deletion)
|
||||
if ENABLE_AUTO_DELETION:
|
||||
folder.protect_from_deletion = True
|
||||
folder.retention_days = -1
|
||||
else:
|
||||
# Regular retention period or null (use global)
|
||||
folder.protect_from_deletion = False
|
||||
folder.retention_days = retention_days if retention_days else None
|
||||
if 'auto_share_on_apply' in data:
|
||||
# Only applicable to group folders
|
||||
if folder.group_id:
|
||||
folder.auto_share_on_apply = bool(data['auto_share_on_apply'])
|
||||
if 'share_with_group_lead' in data:
|
||||
# Only applicable to group folders
|
||||
if folder.group_id:
|
||||
folder.share_with_group_lead = bool(data['share_with_group_lead'])
|
||||
if 'naming_template_id' in data:
|
||||
naming_template_id = data['naming_template_id']
|
||||
if naming_template_id:
|
||||
template = NamingTemplate.query.filter_by(id=naming_template_id, user_id=current_user.id).first()
|
||||
if not template:
|
||||
return jsonify({'error': 'Naming template not found'}), 404
|
||||
folder.naming_template_id = naming_template_id if naming_template_id else None
|
||||
if 'export_template_id' in data:
|
||||
export_template_id = data['export_template_id']
|
||||
if export_template_id:
|
||||
template = ExportTemplate.query.filter_by(id=export_template_id, user_id=current_user.id).first()
|
||||
if not template:
|
||||
return jsonify({'error': 'Export template not found'}), 404
|
||||
folder.export_template_id = export_template_id if export_template_id else None
|
||||
|
||||
folder.updated_at = datetime.utcnow()
|
||||
|
||||
try:
|
||||
db.session.commit()
|
||||
except IntegrityError as e:
|
||||
db.session.rollback()
|
||||
current_app.logger.error(f"Folder update failed due to integrity constraint: {str(e)}")
|
||||
return jsonify({'error': 'A folder with this name already exists'}), 400
|
||||
|
||||
return jsonify(folder.to_dict())
|
||||
|
||||
|
||||
@folders_bp.route('/api/folders/<int:folder_id>', methods=['DELETE'])
|
||||
@login_required
|
||||
def delete_folder(folder_id):
|
||||
"""Delete a folder. Recordings in this folder will have folder_id set to NULL."""
|
||||
# Check if folders feature is enabled
|
||||
folders_enabled = SystemSetting.get_setting('enable_folders', False)
|
||||
if not folders_enabled:
|
||||
return jsonify({'error': 'Folders feature is not enabled'}), 403
|
||||
|
||||
folder = db.session.get(Folder, folder_id)
|
||||
if not folder:
|
||||
return jsonify({'error': 'Folder not found'}), 404
|
||||
|
||||
# Check permissions
|
||||
if folder.group_id:
|
||||
# Group folder - user must be a team admin
|
||||
membership = GroupMembership.query.filter_by(
|
||||
group_id=folder.group_id,
|
||||
user_id=current_user.id
|
||||
).first()
|
||||
|
||||
if not membership or membership.role != 'admin':
|
||||
return jsonify({'error': 'Only group admins can delete group folders'}), 403
|
||||
else:
|
||||
# Personal folder - must belong to the user
|
||||
if folder.user_id != current_user.id:
|
||||
return jsonify({'error': 'You do not have permission to delete this folder'}), 403
|
||||
|
||||
# Recordings in this folder will have folder_id set to NULL via ondelete='SET NULL'
|
||||
db.session.delete(folder)
|
||||
db.session.commit()
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
@folders_bp.route('/api/groups/<int:group_id>/folders', methods=['POST'])
|
||||
@login_required
|
||||
def create_group_folder(group_id):
|
||||
"""Create a group-scoped folder (group admins only)."""
|
||||
# Check if folders feature is enabled
|
||||
folders_enabled = SystemSetting.get_setting('enable_folders', False)
|
||||
if not folders_enabled:
|
||||
return jsonify({'error': 'Folders feature is not enabled'}), 403
|
||||
|
||||
if not ENABLE_INTERNAL_SHARING:
|
||||
return jsonify({'error': 'Group folders require internal sharing to be enabled. Please set ENABLE_INTERNAL_SHARING=true in your configuration.'}), 403
|
||||
|
||||
# Verify team exists
|
||||
team = db.session.get(Group, group_id)
|
||||
if not team:
|
||||
return jsonify({'error': 'Group not found'}), 404
|
||||
|
||||
# Verify user is a team admin
|
||||
membership = GroupMembership.query.filter_by(
|
||||
group_id=group_id,
|
||||
user_id=current_user.id
|
||||
).first()
|
||||
|
||||
if not membership or membership.role != 'admin':
|
||||
return jsonify({'error': 'Only group admins can create group folders'}), 403
|
||||
|
||||
data = request.get_json()
|
||||
name = data.get('name', '').strip()
|
||||
|
||||
if not name:
|
||||
return jsonify({'error': 'Folder name is required'}), 400
|
||||
|
||||
# Check if a group folder with this name already exists for this team
|
||||
existing_folder = Folder.query.filter_by(
|
||||
name=name,
|
||||
group_id=group_id
|
||||
).first()
|
||||
|
||||
if existing_folder:
|
||||
return jsonify({'error': 'A group folder with this name already exists'}), 400
|
||||
|
||||
# Validate naming_template_id if provided
|
||||
naming_template_id = data.get('naming_template_id')
|
||||
if naming_template_id:
|
||||
template = NamingTemplate.query.filter_by(id=naming_template_id, user_id=current_user.id).first()
|
||||
if not template:
|
||||
return jsonify({'error': 'Naming template not found'}), 404
|
||||
|
||||
# Validate export_template_id if provided
|
||||
export_template_id = data.get('export_template_id')
|
||||
if export_template_id:
|
||||
template = ExportTemplate.query.filter_by(id=export_template_id, user_id=current_user.id).first()
|
||||
if not template:
|
||||
return jsonify({'error': 'Export template not found'}), 404
|
||||
|
||||
# Create the group folder with all supported parameters
|
||||
folder = Folder(
|
||||
name=name,
|
||||
user_id=current_user.id, # Creator
|
||||
group_id=group_id,
|
||||
color=data.get('color', '#10B981'),
|
||||
custom_prompt=data.get('custom_prompt'),
|
||||
default_language=data.get('default_language'),
|
||||
default_min_speakers=data.get('default_min_speakers'),
|
||||
default_max_speakers=data.get('default_max_speakers'),
|
||||
default_hotwords=data.get('default_hotwords'),
|
||||
default_initial_prompt=data.get('default_initial_prompt'),
|
||||
protect_from_deletion=data.get('protect_from_deletion', False),
|
||||
retention_days=data.get('retention_days'),
|
||||
auto_share_on_apply=data.get('auto_share_on_apply', True), # Default to True for group folders
|
||||
share_with_group_lead=data.get('share_with_group_lead', True), # Default to True for group folders
|
||||
naming_template_id=naming_template_id,
|
||||
export_template_id=export_template_id
|
||||
)
|
||||
|
||||
db.session.add(folder)
|
||||
|
||||
try:
|
||||
db.session.commit()
|
||||
except IntegrityError as e:
|
||||
db.session.rollback()
|
||||
current_app.logger.error(f"Folder creation failed due to integrity constraint: {str(e)}")
|
||||
return jsonify({'error': 'A folder with this name already exists'}), 400
|
||||
|
||||
return jsonify(folder.to_dict()), 201
|
||||
|
||||
|
||||
@folders_bp.route('/api/groups/<int:group_id>/folders', methods=['GET'])
|
||||
@login_required
|
||||
def get_group_folders(group_id):
|
||||
"""Get all folders for a team (team members only)."""
|
||||
# Check if folders feature is enabled
|
||||
folders_enabled = SystemSetting.get_setting('enable_folders', False)
|
||||
if not folders_enabled:
|
||||
return jsonify({'error': 'Folders feature is not enabled'}), 403
|
||||
|
||||
# Verify team exists
|
||||
team = db.session.get(Group, group_id)
|
||||
if not team:
|
||||
return jsonify({'error': 'Group not found'}), 404
|
||||
|
||||
# Verify user is a team member
|
||||
membership = GroupMembership.query.filter_by(
|
||||
group_id=group_id,
|
||||
user_id=current_user.id
|
||||
).first()
|
||||
|
||||
if not membership:
|
||||
return jsonify({'error': 'You must be a team member to view group folders'}), 403
|
||||
|
||||
# Get all group folders
|
||||
folders = Folder.query.filter_by(group_id=group_id).all()
|
||||
|
||||
return jsonify({'folders': [folder.to_dict() for folder in folders]})
|
||||
|
||||
|
||||
@folders_bp.route('/api/recordings/<int:recording_id>/folder', methods=['PUT'])
|
||||
@login_required
|
||||
def assign_recording_folder(recording_id):
|
||||
"""Assign a recording to a folder (or move to a different folder)."""
|
||||
# Check if folders feature is enabled
|
||||
folders_enabled = SystemSetting.get_setting('enable_folders', False)
|
||||
if not folders_enabled:
|
||||
return jsonify({'error': 'Folders feature is not enabled'}), 403
|
||||
|
||||
recording = db.session.get(Recording, recording_id)
|
||||
if not recording:
|
||||
return jsonify({'error': 'Recording not found'}), 404
|
||||
|
||||
# Check access to recording (require edit permission)
|
||||
if has_recording_access:
|
||||
if not has_recording_access(recording, current_user, require_edit=True):
|
||||
return jsonify({'error': 'You do not have permission to modify this recording'}), 403
|
||||
else:
|
||||
# Fallback: only owner can assign folder
|
||||
if recording.user_id != current_user.id:
|
||||
return jsonify({'error': 'You do not have permission to modify this recording'}), 403
|
||||
|
||||
data = request.get_json()
|
||||
folder_id = data.get('folder_id')
|
||||
|
||||
if folder_id:
|
||||
# Verify folder exists and user has access
|
||||
folder = db.session.get(Folder, folder_id)
|
||||
if not folder:
|
||||
return jsonify({'error': 'Folder not found'}), 404
|
||||
|
||||
# Check if user can use this folder
|
||||
if folder.group_id:
|
||||
# Group folder - user must be a member
|
||||
membership = GroupMembership.query.filter_by(
|
||||
group_id=folder.group_id,
|
||||
user_id=current_user.id
|
||||
).first()
|
||||
if not membership:
|
||||
return jsonify({'error': 'You do not have access to this folder'}), 403
|
||||
else:
|
||||
# Personal folder - must be owner
|
||||
if folder.user_id != current_user.id:
|
||||
return jsonify({'error': 'You do not have access to this folder'}), 403
|
||||
|
||||
# Handle auto-sharing for group folders
|
||||
old_folder_id = recording.folder_id
|
||||
recording.folder_id = folder_id
|
||||
|
||||
# Apply auto-shares if moving to a group folder
|
||||
if folder.group_id and (folder.auto_share_on_apply or folder.share_with_group_lead):
|
||||
_apply_folder_auto_shares(recording, folder)
|
||||
|
||||
audit_access('move_folder', 'recording', recording_id, details={'folder_id': folder_id, 'old_folder_id': old_folder_id})
|
||||
db.session.commit() # commit folder + audit en une transaction atomique
|
||||
current_app.logger.info(f"Recording {recording_id} moved to folder {folder_id} by user {current_user.id}")
|
||||
else:
|
||||
# Remove from folder
|
||||
recording.folder_id = None
|
||||
db.session.commit()
|
||||
current_app.logger.info(f"Recording {recording_id} removed from folder by user {current_user.id}")
|
||||
|
||||
return jsonify(recording.to_dict(include_html=False, viewer_user=current_user))
|
||||
|
||||
|
||||
@folders_bp.route('/api/recordings/<int:recording_id>/folder', methods=['DELETE'])
|
||||
@login_required
|
||||
def remove_recording_folder(recording_id):
|
||||
"""Remove a recording from its folder."""
|
||||
# Check if folders feature is enabled
|
||||
folders_enabled = SystemSetting.get_setting('enable_folders', False)
|
||||
if not folders_enabled:
|
||||
return jsonify({'error': 'Folders feature is not enabled'}), 403
|
||||
|
||||
recording = db.session.get(Recording, recording_id)
|
||||
if not recording:
|
||||
return jsonify({'error': 'Recording not found'}), 404
|
||||
|
||||
# Check access to recording (require edit permission)
|
||||
if has_recording_access:
|
||||
if not has_recording_access(recording, current_user, require_edit=True):
|
||||
return jsonify({'error': 'You do not have permission to modify this recording'}), 403
|
||||
else:
|
||||
# Fallback: only owner can remove folder
|
||||
if recording.user_id != current_user.id:
|
||||
return jsonify({'error': 'You do not have permission to modify this recording'}), 403
|
||||
|
||||
recording.folder_id = None
|
||||
db.session.commit()
|
||||
current_app.logger.info(f"Recording {recording_id} removed from folder by user {current_user.id}")
|
||||
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
@folders_bp.route('/api/recordings/bulk/folder', methods=['POST'])
|
||||
@login_required
|
||||
def bulk_assign_folder():
|
||||
"""Assign multiple recordings to a folder."""
|
||||
# Check if folders feature is enabled
|
||||
folders_enabled = SystemSetting.get_setting('enable_folders', False)
|
||||
if not folders_enabled:
|
||||
return jsonify({'error': 'Folders feature is not enabled'}), 403
|
||||
|
||||
data = request.get_json()
|
||||
recording_ids = data.get('recording_ids', [])
|
||||
folder_id = data.get('folder_id') # Can be None to remove from folder
|
||||
|
||||
if not recording_ids:
|
||||
return jsonify({'error': 'No recordings specified'}), 400
|
||||
|
||||
# Verify folder if specified
|
||||
folder = None
|
||||
if folder_id:
|
||||
folder = db.session.get(Folder, folder_id)
|
||||
if not folder:
|
||||
return jsonify({'error': 'Folder not found'}), 404
|
||||
|
||||
# Check if user can use this folder
|
||||
if folder.group_id:
|
||||
membership = GroupMembership.query.filter_by(
|
||||
group_id=folder.group_id,
|
||||
user_id=current_user.id
|
||||
).first()
|
||||
if not membership:
|
||||
return jsonify({'error': 'You do not have access to this folder'}), 403
|
||||
else:
|
||||
if folder.user_id != current_user.id:
|
||||
return jsonify({'error': 'You do not have access to this folder'}), 403
|
||||
|
||||
updated_count = 0
|
||||
for rec_id in recording_ids:
|
||||
recording = db.session.get(Recording, rec_id)
|
||||
if not recording:
|
||||
continue
|
||||
|
||||
# Check access (require edit permission)
|
||||
if has_recording_access:
|
||||
if not has_recording_access(recording, current_user, require_edit=True):
|
||||
continue
|
||||
else:
|
||||
if recording.user_id != current_user.id:
|
||||
continue
|
||||
|
||||
recording.folder_id = folder_id
|
||||
|
||||
# Apply auto-shares if moving to a group folder
|
||||
if folder and folder.group_id and (folder.auto_share_on_apply or folder.share_with_group_lead):
|
||||
_apply_folder_auto_shares(recording, folder)
|
||||
|
||||
updated_count += 1
|
||||
|
||||
db.session.commit()
|
||||
action = f"moved to folder {folder_id}" if folder_id else "removed from folder"
|
||||
current_app.logger.info(f"Bulk folder update: {updated_count} recordings {action} by user {current_user.id}")
|
||||
|
||||
return jsonify({'success': True, 'updated_count': updated_count})
|
||||
|
||||
|
||||
def _apply_folder_auto_shares(recording, folder):
|
||||
"""
|
||||
Apply auto-shares for a group folder when a recording is assigned to it.
|
||||
|
||||
Args:
|
||||
recording: Recording being assigned to the folder
|
||||
folder: Folder with auto-share settings
|
||||
"""
|
||||
if not ENABLE_INTERNAL_SHARING:
|
||||
return
|
||||
|
||||
if not folder.group_id:
|
||||
return
|
||||
|
||||
# Determine who to share with
|
||||
if folder.auto_share_on_apply:
|
||||
group_members = GroupMembership.query.filter_by(group_id=folder.group_id).all()
|
||||
elif folder.share_with_group_lead:
|
||||
group_members = GroupMembership.query.filter_by(group_id=folder.group_id, role='admin').all()
|
||||
else:
|
||||
return
|
||||
|
||||
shares_created = 0
|
||||
|
||||
for membership in group_members:
|
||||
# Skip the recording owner
|
||||
if membership.user_id == recording.user_id:
|
||||
continue
|
||||
|
||||
# Check if already shared
|
||||
existing_share = InternalShare.query.filter_by(
|
||||
recording_id=recording.id,
|
||||
shared_with_user_id=membership.user_id
|
||||
).first()
|
||||
|
||||
if not existing_share:
|
||||
# Create internal share with correct permissions
|
||||
share = InternalShare(
|
||||
recording_id=recording.id,
|
||||
owner_id=recording.user_id,
|
||||
shared_with_user_id=membership.user_id,
|
||||
can_edit=(membership.role == 'admin'),
|
||||
can_reshare=False,
|
||||
source_type='group_folder',
|
||||
source_tag_id=None # We don't use this field for folders
|
||||
)
|
||||
db.session.add(share)
|
||||
|
||||
# Create SharedRecordingState with default values for the recipient
|
||||
state = SharedRecordingState(
|
||||
recording_id=recording.id,
|
||||
user_id=membership.user_id,
|
||||
is_inbox=True,
|
||||
is_highlighted=False
|
||||
)
|
||||
db.session.add(state)
|
||||
|
||||
shares_created += 1
|
||||
current_app.logger.info(f"Auto-shared recording {recording.id} with user {membership.user_id} via group folder '{folder.name}'")
|
||||
|
||||
if shares_created > 0:
|
||||
current_app.logger.info(f"Created {shares_created} auto-shares for recording {recording.id} via folder assignment")
|
||||
394
src/api/groups.py
Normal file
394
src/api/groups.py
Normal file
@@ -0,0 +1,394 @@
|
||||
"""
|
||||
Group management and collaboration.
|
||||
|
||||
This blueprint was auto-generated from app.py route extraction.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify, send_file, Response, current_app
|
||||
from flask_login import login_required, current_user
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from src.database import db
|
||||
from src.models import *
|
||||
from src.utils import *
|
||||
|
||||
# Create blueprint
|
||||
groups_bp = Blueprint('groups', __name__)
|
||||
|
||||
# Configuration from environment
|
||||
ENABLE_INQUIRE_MODE = os.environ.get('ENABLE_INQUIRE_MODE', 'false').lower() == 'true'
|
||||
ENABLE_AUTO_DELETION = os.environ.get('ENABLE_AUTO_DELETION', 'false').lower() == 'true'
|
||||
USERS_CAN_DELETE = os.environ.get('USERS_CAN_DELETE', 'true').lower() == 'true'
|
||||
ENABLE_INTERNAL_SHARING = os.environ.get('ENABLE_INTERNAL_SHARING', 'false').lower() == 'true'
|
||||
USE_ASR_ENDPOINT = os.environ.get('USE_ASR_ENDPOINT', 'false').lower() == 'true'
|
||||
|
||||
# Global helpers (will be injected from app)
|
||||
has_recording_access = None
|
||||
bcrypt = None
|
||||
csrf = None
|
||||
limiter = None
|
||||
|
||||
def init_groups_helpers(**kwargs):
|
||||
"""Initialize helper functions and extensions from app."""
|
||||
global has_recording_access, bcrypt, csrf, limiter
|
||||
has_recording_access = kwargs.get('has_recording_access')
|
||||
bcrypt = kwargs.get('bcrypt')
|
||||
csrf = kwargs.get('csrf')
|
||||
limiter = kwargs.get('limiter')
|
||||
|
||||
|
||||
# --- Routes ---
|
||||
|
||||
@groups_bp.route('/api/groups/<int:group_id>/sync-shares', methods=['POST'])
|
||||
@login_required
|
||||
def sync_team_tag_shares(group_id):
|
||||
"""Retroactively share recordings with group members based on group tags with auto-sharing enabled."""
|
||||
# Verify group exists
|
||||
group = db.session.get(Group, group_id)
|
||||
if not group:
|
||||
return jsonify({'error': 'Group not found'}), 404
|
||||
|
||||
# Verify user is a group admin
|
||||
membership = GroupMembership.query.filter_by(
|
||||
group_id=group_id,
|
||||
user_id=current_user.id,
|
||||
role='admin'
|
||||
).first()
|
||||
|
||||
if not membership:
|
||||
return jsonify({'error': 'Only group admins can sync shares'}), 403
|
||||
|
||||
if not ENABLE_INTERNAL_SHARING:
|
||||
return jsonify({'error': 'Internal sharing is not enabled'}), 403
|
||||
|
||||
# Get all group tags with auto-sharing enabled
|
||||
group_tags = Tag.query.filter(
|
||||
Tag.group_id == group_id,
|
||||
db.or_(
|
||||
Tag.auto_share_on_apply == True,
|
||||
Tag.share_with_group_lead == True
|
||||
)
|
||||
).all()
|
||||
|
||||
shares_created = 0
|
||||
recordings_processed = 0
|
||||
|
||||
for tag in group_tags:
|
||||
# Get all completed recordings with this tag
|
||||
recordings = db.session.query(Recording).join(RecordingTag).filter(
|
||||
RecordingTag.tag_id == tag.id,
|
||||
Recording.status == 'COMPLETED'
|
||||
).all()
|
||||
|
||||
for recording in recordings:
|
||||
recordings_processed += 1
|
||||
|
||||
# Determine who to share with
|
||||
if tag.auto_share_on_apply:
|
||||
group_members = GroupMembership.query.filter_by(group_id=group_id).all()
|
||||
elif tag.share_with_group_lead:
|
||||
group_members = GroupMembership.query.filter_by(group_id=group_id, role='admin').all()
|
||||
else:
|
||||
continue
|
||||
|
||||
for membership_to_share in group_members:
|
||||
# Skip the recording owner
|
||||
if membership_to_share.user_id == recording.user_id:
|
||||
continue
|
||||
|
||||
# Check if already shared
|
||||
existing_share = InternalShare.query.filter_by(
|
||||
recording_id=recording.id,
|
||||
shared_with_user_id=membership_to_share.user_id
|
||||
).first()
|
||||
|
||||
if not existing_share:
|
||||
# Create internal share with correct permissions
|
||||
# Group admins get edit permission, regular members get read-only
|
||||
share = InternalShare(
|
||||
recording_id=recording.id,
|
||||
owner_id=recording.user_id,
|
||||
shared_with_user_id=membership_to_share.user_id,
|
||||
can_edit=(membership_to_share.role == 'admin'),
|
||||
can_reshare=False,
|
||||
source_type='group_tag',
|
||||
source_tag_id=tag.id
|
||||
)
|
||||
db.session.add(share)
|
||||
|
||||
# Create SharedRecordingState with default values for the recipient
|
||||
state = SharedRecordingState(
|
||||
recording_id=recording.id,
|
||||
user_id=membership_to_share.user_id,
|
||||
is_inbox=True, # New shares appear in inbox by default
|
||||
is_highlighted=False # Not favorited by default
|
||||
)
|
||||
db.session.add(state)
|
||||
|
||||
shares_created += 1
|
||||
current_app.logger.info(f"Synced share: Recording {recording.id} with user {membership_to_share.user_id} (role={membership_to_share.role}) via group tag '{tag.name}'")
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'shares_created': shares_created,
|
||||
'recordings_processed': recordings_processed,
|
||||
'message': f'Created {shares_created} new shares across {recordings_processed} recordings'
|
||||
})
|
||||
|
||||
|
||||
|
||||
@groups_bp.route('/api/admin/groups', methods=['GET'])
|
||||
@login_required
|
||||
def get_teams():
|
||||
"""Get all groups (admin) or groups user is admin of (group admin)."""
|
||||
# Check if user is admin OR group admin
|
||||
is_group_admin = GroupMembership.query.filter_by(
|
||||
user_id=current_user.id,
|
||||
role='admin'
|
||||
).first() is not None
|
||||
|
||||
if not current_user.is_admin and not is_group_admin:
|
||||
return jsonify({'error': 'Admin access required'}), 403
|
||||
|
||||
# If full admin, return all groups; if group admin, return only their groups
|
||||
if current_user.is_admin:
|
||||
groups = Group.query.all()
|
||||
else:
|
||||
# Get groups where user is an admin
|
||||
group_memberships = GroupMembership.query.filter_by(
|
||||
user_id=current_user.id,
|
||||
role='admin'
|
||||
).all()
|
||||
groups = [m.group for m in group_memberships]
|
||||
|
||||
return jsonify({'groups': [group.to_dict() for group in groups]})
|
||||
|
||||
|
||||
|
||||
@groups_bp.route('/api/admin/groups', methods=['POST'])
|
||||
@login_required
|
||||
def create_team():
|
||||
"""Create a new group (admin only)."""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'error': 'Admin access required'}), 403
|
||||
|
||||
if not ENABLE_INTERNAL_SHARING:
|
||||
return jsonify({'error': 'Groups require internal sharing to be enabled. Please set ENABLE_INTERNAL_SHARING=true in your configuration.'}), 403
|
||||
|
||||
data = request.get_json()
|
||||
name = data.get('name', '').strip()
|
||||
description = data.get('description', '').strip()
|
||||
|
||||
if not name:
|
||||
return jsonify({'error': 'Group name is required'}), 400
|
||||
|
||||
# Check if group name already exists
|
||||
existing = Group.query.filter_by(name=name).first()
|
||||
if existing:
|
||||
return jsonify({'error': 'A group with this name already exists'}), 400
|
||||
|
||||
group = Group(name=name, description=description)
|
||||
db.session.add(group)
|
||||
db.session.commit()
|
||||
|
||||
current_app.logger.info(f"Admin {current_user.username} created group: {name}")
|
||||
return jsonify(group.to_dict()), 201
|
||||
|
||||
|
||||
|
||||
@groups_bp.route('/api/admin/groups/<int:group_id>', methods=['GET'])
|
||||
@login_required
|
||||
def get_team(group_id):
|
||||
"""Get group details (admin or group admin)."""
|
||||
group = db.session.get(Group, group_id)
|
||||
if not group:
|
||||
return jsonify({'error': 'Group not found'}), 404
|
||||
|
||||
# Check if user is admin OR admin of this specific group
|
||||
is_group_admin = GroupMembership.query.filter_by(
|
||||
group_id=group_id,
|
||||
user_id=current_user.id,
|
||||
role='admin'
|
||||
).first() is not None
|
||||
|
||||
if not current_user.is_admin and not is_group_admin:
|
||||
return jsonify({'error': 'Admin access required'}), 403
|
||||
|
||||
group_dict = group.to_dict()
|
||||
group_dict['members'] = [m.to_dict() for m in group.memberships]
|
||||
return jsonify(group_dict)
|
||||
|
||||
|
||||
|
||||
@groups_bp.route('/api/admin/groups/<int:group_id>', methods=['PUT'])
|
||||
@login_required
|
||||
def update_team(group_id):
|
||||
"""Update group (admin or group admin)."""
|
||||
group = db.session.get(Group, group_id)
|
||||
if not group:
|
||||
return jsonify({'error': 'Group not found'}), 404
|
||||
|
||||
# Check if user is admin OR admin of this specific group
|
||||
is_group_admin = GroupMembership.query.filter_by(
|
||||
group_id=group_id,
|
||||
user_id=current_user.id,
|
||||
role='admin'
|
||||
).first() is not None
|
||||
|
||||
if not current_user.is_admin and not is_group_admin:
|
||||
return jsonify({'error': 'Admin access required'}), 403
|
||||
|
||||
data = request.get_json()
|
||||
name = data.get('name', '').strip()
|
||||
description = data.get('description', '').strip()
|
||||
|
||||
if name:
|
||||
# Check if new name conflicts with another group
|
||||
existing = Group.query.filter(Group.name == name, Group.id != group_id).first()
|
||||
if existing:
|
||||
return jsonify({'error': 'A group with this name already exists'}), 400
|
||||
group.name = name
|
||||
|
||||
group.description = description
|
||||
db.session.commit()
|
||||
|
||||
current_app.logger.info(f"Admin {current_user.username} updated group: {group.name}")
|
||||
return jsonify(group.to_dict())
|
||||
|
||||
|
||||
|
||||
@groups_bp.route('/api/admin/groups/<int:group_id>', methods=['DELETE'])
|
||||
@login_required
|
||||
def delete_team(group_id):
|
||||
"""Delete group (admin only)."""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({'error': 'Admin access required'}), 403
|
||||
|
||||
group = db.session.get(Group, group_id)
|
||||
if not group:
|
||||
return jsonify({'error': 'Group not found'}), 404
|
||||
|
||||
group_name = group.name
|
||||
db.session.delete(group)
|
||||
db.session.commit()
|
||||
|
||||
current_app.logger.info(f"Admin {current_user.username} deleted group: {group_name}")
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
|
||||
@groups_bp.route('/api/admin/groups/<int:group_id>/members', methods=['POST'])
|
||||
@login_required
|
||||
def add_team_member(group_id):
|
||||
"""Add a member to a group (admin or group admin)."""
|
||||
if not ENABLE_INTERNAL_SHARING:
|
||||
return jsonify({'error': 'Groups require internal sharing to be enabled. Please set ENABLE_INTERNAL_SHARING=true in your configuration.'}), 403
|
||||
|
||||
group = db.session.get(Group, group_id)
|
||||
if not group:
|
||||
return jsonify({'error': 'Group not found'}), 404
|
||||
|
||||
# Check if user is admin OR admin of this specific group
|
||||
is_group_admin = GroupMembership.query.filter_by(
|
||||
group_id=group_id,
|
||||
user_id=current_user.id,
|
||||
role='admin'
|
||||
).first() is not None
|
||||
|
||||
if not current_user.is_admin and not is_group_admin:
|
||||
return jsonify({'error': 'Admin access required'}), 403
|
||||
|
||||
data = request.get_json()
|
||||
user_id = data.get('user_id')
|
||||
role = data.get('role', 'member')
|
||||
|
||||
if not user_id:
|
||||
return jsonify({'error': 'User ID is required'}), 400
|
||||
|
||||
if role not in ['admin', 'member']:
|
||||
return jsonify({'error': 'Role must be "admin" or "member"'}), 400
|
||||
|
||||
user = db.session.get(User, user_id)
|
||||
if not user:
|
||||
return jsonify({'error': 'User not found'}), 404
|
||||
|
||||
# Check if already a member
|
||||
existing = GroupMembership.query.filter_by(group_id=group_id, user_id=user_id).first()
|
||||
if existing:
|
||||
return jsonify({'error': 'User is already a member of this group'}), 400
|
||||
|
||||
membership = GroupMembership(group_id=group_id, user_id=user_id, role=role)
|
||||
db.session.add(membership)
|
||||
db.session.commit()
|
||||
|
||||
current_app.logger.info(f"Admin {current_user.username} added {user.username} to group {group.name} as {role}")
|
||||
return jsonify(membership.to_dict()), 201
|
||||
|
||||
|
||||
|
||||
@groups_bp.route('/api/admin/groups/<int:group_id>/members/<int:user_id>', methods=['PUT'])
|
||||
@login_required
|
||||
def update_team_member(group_id, user_id):
|
||||
"""Update group member role (admin or group admin)."""
|
||||
membership = GroupMembership.query.filter_by(group_id=group_id, user_id=user_id).first()
|
||||
if not membership:
|
||||
return jsonify({'error': 'Membership not found'}), 404
|
||||
|
||||
# Check if user is admin OR admin of this specific group
|
||||
is_group_admin = GroupMembership.query.filter_by(
|
||||
group_id=group_id,
|
||||
user_id=current_user.id,
|
||||
role='admin'
|
||||
).first() is not None
|
||||
|
||||
if not current_user.is_admin and not is_group_admin:
|
||||
return jsonify({'error': 'Admin access required'}), 403
|
||||
|
||||
data = request.get_json()
|
||||
role = data.get('role')
|
||||
|
||||
if role not in ['admin', 'member']:
|
||||
return jsonify({'error': 'Role must be "admin" or "member"'}), 400
|
||||
|
||||
membership.role = role
|
||||
db.session.commit()
|
||||
|
||||
current_app.logger.info(f"Admin {current_user.username} updated {membership.user.username} role to {role} in group {membership.group.name}")
|
||||
return jsonify(membership.to_dict())
|
||||
|
||||
|
||||
|
||||
@groups_bp.route('/api/admin/groups/<int:group_id>/members/<int:user_id>', methods=['DELETE'])
|
||||
@login_required
|
||||
def remove_team_member(group_id, user_id):
|
||||
"""Remove a member from a group (admin or group admin)."""
|
||||
membership = GroupMembership.query.filter_by(group_id=group_id, user_id=user_id).first()
|
||||
if not membership:
|
||||
return jsonify({'error': 'Membership not found'}), 404
|
||||
|
||||
# Check if user is admin OR admin of this specific group
|
||||
is_group_admin = GroupMembership.query.filter_by(
|
||||
group_id=group_id,
|
||||
user_id=current_user.id,
|
||||
role='admin'
|
||||
).first() is not None
|
||||
|
||||
if not current_user.is_admin and not is_group_admin:
|
||||
return jsonify({'error': 'Admin access required'}), 403
|
||||
|
||||
username = membership.user.username
|
||||
group_name = membership.group.name
|
||||
db.session.delete(membership)
|
||||
db.session.commit()
|
||||
|
||||
current_app.logger.info(f"Admin {current_user.username} removed {username} from group {group_name}")
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user