Initial release: DictIA v0.8.14-alpha (fork de Speakr, AGPL-3.0)

This commit is contained in:
InnovA AI
2026-03-16 21:47:37 +00:00
commit 42772a31ed
365 changed files with 103572 additions and 0 deletions

63
.dockerignore Normal file
View 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
View 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
View 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
View File

@@ -0,0 +1 @@
v0.8.14-alpha

121
CHANGES.md Normal file
View 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
View 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
View 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
View 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
View 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

1
VERSION Normal file
View File

@@ -0,0 +1 @@
v0.8.14-alpha

View 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*).

View 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.

View 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*).

View 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*).

View 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) →

View 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) →

View 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) →

View 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)

View 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) →

View 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) →

View 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) →

View 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) →

View 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) →

View 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) →

View 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.

View 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)

View 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)

View 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)

View 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>

View 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)

View 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)

View 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)

View 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)

View 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
View 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.

View 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
View 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
View 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
View 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

View 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
View 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
View 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
View File

@@ -0,0 +1 @@
scipy<1.15

105
deployment/README.md Normal file
View 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
View File

@@ -0,0 +1,5 @@
gcp-credentials.json
usage-stats.json
venv/
__pycache__/
*.pyc

View 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

File diff suppressed because it is too large Load Diff

View 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)

View 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

View 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"

View 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;
# }
# }

View 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

View 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"

View 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

View 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

View 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

View 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

View 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)
```

View 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
```

View 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
```

View 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
```

View 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

View 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

View 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

View 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

View 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

View File

@@ -0,0 +1,8 @@
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"storage-driver": "overlay2"
}

View 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

View 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
View 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

View 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 ==="

View 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
View 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
View 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

View File

@@ -0,0 +1,2 @@
sentence-transformers==2.7.0
huggingface-hub>=0.19.0

25
requirements.txt Normal file
View 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
View 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()

View 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 "$@"

View 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()

View 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
View 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

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

107
src/api/audit.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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