diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..1a676efb --- /dev/null +++ b/.dockerignore @@ -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 diff --git a/.env.example b/.env.example deleted file mode 100644 index 32f662f1..00000000 --- a/.env.example +++ /dev/null @@ -1,21 +0,0 @@ -# OpenRouter API Configuration -OPENROUTER_API_KEY=your_openrouter_api_key_here -OPENROUTER_BASE_URL=https://openrouter.ai/api/v1 -# Choose a model compatible with function calling / JSON mode on OpenRouter -# Example: "openai/gpt-3.5-turbo" or "google/gemini-pro" (check OpenRouter docs for compatible models) -OPENROUTER_MODEL_NAME="openai/gpt-4o-mini" - -# OpenAI API Configuration (for transcription) -TRANSCRIPTION_API_KEY=your_openai_api_key_here -TRANSCRIPTION_BASE_URL=http://your_local_api_url:port/v1/ - -# Whisper Model Configuration -# Default: "Systran/faster-distil-whisper-large-v3" -WHISPER_MODEL="Systran/faster-distil-whisper-large-v3" - -# Application Settings -# Set to "false" to disable new account registration -ALLOW_REGISTRATION="true" - -# Security -SECRET_KEY=your_secret_key_here diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..8653a3fc --- /dev/null +++ b/.gitattributes @@ -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 + diff --git a/.github/CLA-SETUP.md b/.github/CLA-SETUP.md new file mode 100644 index 00000000..e008176f --- /dev/null +++ b/.github/CLA-SETUP.md @@ -0,0 +1,200 @@ +# CLA Assistant Setup Instructions + +This document explains how to set up automated CLA checking for Speakr. + +## What We're Using + +**CLA Assistant** - A free, open-source GitHub Action that automatically: +- Comments on PRs asking contributors to sign the CLA +- Tracks who has signed +- Blocks PR merging until CLA is signed +- Stores signatures in a JSON file + +## Setup Steps + +### 1. Create a Personal Access Token (PAT) + +The CLA Assistant needs a GitHub Personal Access Token to create commits for storing signatures. + +1. Go to GitHub Settings → Developer Settings → Personal Access Tokens → Tokens (classic) + - Or visit: https://github.com/settings/tokens + +2. Click "Generate new token" → "Generate new token (classic)" + +3. Give it a descriptive name: `Speakr CLA Assistant` + +4. Set expiration: `No expiration` (or your preferred duration) + +5. Select scopes: + - ✅ `repo` (Full control of private repositories) + - ✅ `workflow` (Update GitHub Action workflows) + +6. Click "Generate token" + +7. **IMPORTANT**: Copy the token immediately (you won't see it again!) + +### 2. Add Token to Repository Secrets + +1. Go to your repository: https://github.com/murtaza-nasir/speakr + +2. Navigate to: Settings → Secrets and variables → Actions + +3. Click "New repository secret" + +4. Name: `PERSONAL_ACCESS_TOKEN` + +5. Value: Paste the token you copied in step 1 + +6. Click "Add secret" + +### 3. Commit the CLA Files + +The files have been created locally. Commit them: + +```bash +git add CLA.md CONTRIBUTING.md .github/workflows/cla.yml .github/CLA-SETUP.md +git commit -m "Add Contributor License Agreement and automated CLA checking" +git push +``` + +### 4. Create the Signatures Branch + +The CLA Assistant will store signatures in a separate branch: + +```bash +# Create and push the signatures branch +git checkout -b cla-signatures +git push -u origin cla-signatures +git checkout master # or main +``` + +### 5. Update README.md (Optional but Recommended) + +Add a badge to show CLA status. Add this near the top of README.md: + +```markdown +[![CLA assistant](https://cla-assistant.io/readme/badge/murtaza-nasir/speakr)](https://cla-assistant.io/murtaza-nasir/speakr) +``` + +Add a link in the Contributing section: + +```markdown +## Contributing + +We welcome contributions! Please read our [Contributing Guide](CONTRIBUTING.md) to learn about our CLA process and development workflow. +``` + +### 6. Test the Setup + +1. Create a test PR from a different account or ask someone to create one + +2. The CLA bot should automatically comment asking for signature + +3. They sign by commenting: `I have read the CLA Document and I hereby sign the CLA` + +4. The bot updates the PR with a success message + +## How It Works + +### For Contributors + +1. They open a PR +2. Bot comments with CLA instructions +3. They read [CLA.md](../CLA.md) +4. They comment: `I have read the CLA Document and I hereby sign the CLA` +5. Bot records signature in `.github/signatures/cla.json` on `cla-signatures` branch +6. Bot marks PR as CLA-signed ✅ +7. Future PRs from same user are auto-approved + +### For Maintainers + +- You'll see CLA status checks on PRs +- Signatures are stored in `.github/signatures/cla.json` +- You can manually check signatures anytime +- Merging is only possible after CLA is signed + +## Viewing Signatures + +All signatures are stored in: +``` +https://github.com/murtaza-nasir/speakr/blob/cla-signatures/.github/signatures/cla.json +``` + +Format: +```json +{ + "signedContributors": [ + { + "name": "username", + "id": 12345, + "comment_id": 67890, + "created_at": "2025-01-18T12:34:56Z", + "repoId": 123456789, + "pullRequestNo": 42 + } + ] +} +``` + +## Troubleshooting + +### Bot Not Commenting on PRs + +- Check that PERSONAL_ACCESS_TOKEN secret is set +- Verify token has correct permissions +- Check GitHub Actions are enabled for repo +- Look at Actions tab for error logs + +### "Branch 'cla-signatures' not found" + +```bash +git checkout -b cla-signatures +git push -u origin cla-signatures +git checkout master +``` + +### Need to Reset Signatures + +To remove all signatures (use carefully!): +```bash +git checkout cla-signatures +rm .github/signatures/cla.json +git commit -m "Reset CLA signatures" +git push +``` + +### Want Someone to Re-sign + +Delete their entry from `.github/signatures/cla.json` and commit: +```bash +git checkout cla-signatures +# Edit .github/signatures/cla.json to remove the user +git add .github/signatures/cla.json +git commit -m "Remove CLA signature for username" +git push +git checkout master +``` + +## Customization + +You can customize the CLA bot messages by editing `.github/workflows/cla.yml`: + +- `custom-notsigned-prcomment` - Message shown to unsigned contributors +- `custom-pr-sign-comment` - Message after signing +- `custom-allsigned-prcomment` - Message when all have signed +- `allowlist` - Users who don't need to sign (bots, etc.) + +## Alternative: Lighter Weight DCO + +If you want something simpler, consider using **Developer Certificate of Origin (DCO)** instead: +- Contributors add `Signed-off-by: Name ` to commits +- No separate signature required +- Less formal but still legally binding +- Used by Linux kernel and many projects + +Let me know if you'd prefer DCO instead! + +## Support + +- CLA Assistant Docs: https://github.com/contributor-assistant/github-action +- Issues with setup? Open an issue in this repo diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml new file mode 100644 index 00000000..fe77eb84 --- /dev/null +++ b/.github/workflows/cla.yml @@ -0,0 +1,39 @@ +name: "CLA Reminder" +on: + pull_request_target: + types: [opened] + +permissions: + pull-requests: write + +jobs: + cla-reminder: + if: github.event_name == 'pull_request_target' + runs-on: ubuntu-latest + steps: + - name: "Post CLA Reminder" + uses: actions/github-script@v8 + with: + script: | + const prNumber = context.payload.pull_request?.number; + if (!prNumber) { + console.log('No PR number found, skipping'); + return; + } + const body = `Thank you for your contribution! + + By submitting this pull request, you agree to the terms of our [Contributor License Agreement (CLA)](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/master/CLA.md). + + **Key points:** + - You retain copyright ownership of your contribution + - You grant us permission to use your contribution under our dual-license model (AGPLv3 and Commercial) + - This allows us to include your contribution in both the open source and commercial versions of Speakr + + If you have any questions about the CLA, please let us know!`.replace(/^ {12}/gm, ''); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: body + }); diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 00000000..b2e17abc --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,178 @@ +name: Docker Build and Publish + +on: + release: + types: [published] + workflow_dispatch: + +env: + # Use docker.io for Docker Hub if empty + REGISTRY: docker.io + # Use explicit image name instead of github.repository + IMAGE_NAME: learnedmachine/speakr + +jobs: + test: + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:16 + env: + POSTGRES_USER: speakr + POSTGRES_PASSWORD: speakr + POSTGRES_DB: speakr_test + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U speakr" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.11' + + - name: Install dependencies + run: pip install -r requirements.txt + + - name: Run static migration compatibility tests + run: python tests/test_migration_compatibility.py + + - name: Run migrations against PostgreSQL + env: + TEST_DATABASE_URI: postgresql://speakr:speakr@localhost:5432/speakr_test + run: python tests/test_postgres_migrations.py + + build: + needs: test + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + # This is used to complete the identity challenge + # with sigstore/fulcio when running outside of PRs. + id-token: write + + steps: + # Free up disk space on the runner (~30GB) + - name: Free disk space + run: | + sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache + sudo docker image prune -af + df -h / + + - name: Checkout repository + uses: actions/checkout@v5 + + # Set up QEMU for multi-platform builds + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + # Set up Docker Buildx + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + # Login to Docker Hub + - name: Log into registry ${{ env.REGISTRY }} + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + # Extract metadata (tags, labels) for Docker + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=match,pattern=v(.*),group=1 + type=sha + type=raw,value=latest,enable=${{ github.event_name == 'workflow_dispatch' || github.event_name == 'release' }} + + # Build and push full Docker image + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + PRODUCTION=1 + + build-lite: + needs: test + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write + + steps: + # Free up disk space on the runner (~30GB) + - name: Free disk space + run: | + sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache + sudo docker image prune -af + df -h / + + - name: Checkout repository + uses: actions/checkout@v5 + + # Set up QEMU for multi-platform builds + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + # Set up Docker Buildx + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + # Login to Docker Hub + - name: Log into registry ${{ env.REGISTRY }} + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + # Extract metadata for lite image + - name: Extract Docker metadata (lite) + id: meta-lite + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=lite,enable=${{ github.event_name == 'workflow_dispatch' || github.event_name == 'release' }} + type=match,pattern=v(.*),group=1,suffix=-lite + type=sha,suffix=-lite + + # Build and push lightweight Docker image (no PyTorch/sentence-transformers) + - name: Build and push lite Docker image + id: build-and-push-lite + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta-lite.outputs.tags }} + labels: ${{ steps.meta-lite.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + PRODUCTION=1 + LIGHTWEIGHT=1 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..053ab34d --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,64 @@ +name: Deploy Documentation + +on: + release: + types: [published] + push: + tags: [ 'v*.*.*' ] + paths: + - 'docs/**' + - '.github/workflows/docs.yml' + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version: '3.11' + + - name: Install MkDocs and dependencies + run: | + pip install --upgrade pip + pip install -r docs/requirements-docs.txt + + - name: Build documentation + env: + CI: true # This enables git-revision-date plugin in CI + run: | + cd docs + # Update site_url for GitHub Pages if needed + if [ "${{ github.repository }}" != "murtaza-nasir/speakr" ]; then + sed -i "s|site_url:.*|site_url: https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}|" mkdocs.yml + fi + mkdocs build --strict --site-dir _site + + - name: Upload artifact + uses: actions/upload-pages-artifact@v5 + with: + path: ./docs/_site + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v5 \ No newline at end of file diff --git a/.github/workflows/docs1.yml b/.github/workflows/docs1.yml new file mode 100644 index 00000000..cd9a3bff --- /dev/null +++ b/.github/workflows/docs1.yml @@ -0,0 +1,61 @@ +name: Deploy Documentation + +on: + push: + branches: [ master, main ] + paths: + - 'docs/**' + - '.github/workflows/docs.yml' + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version: '3.11' + + - name: Install MkDocs and dependencies + run: | + pip install --upgrade pip + pip install -r docs/requirements-docs.txt + + - name: Build documentation + env: + CI: true # This enables git-revision-date plugin in CI + run: | + # Update site_url for GitHub Pages if needed + if [ "${{ github.repository }}" != "murtaza-nasir/speakr" ]; then + sed -i "s|site_url:.*|site_url: https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}|" mkdocs.yml + fi + mkdocs build --strict + + - name: Upload artifact + uses: actions/upload-pages-artifact@v5 + with: + path: ./site + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v5 diff --git a/.gitignore b/.gitignore index 79420b2e..2017e92f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.playwright-mcp/ venv/ __pycache__/ instance/ @@ -7,4 +8,45 @@ uploads/ __pycache__/ *.pyc *.log -.env \ No newline at end of file +*.env +.migrate/ +project_files.md +*.md +notes.md +docker-compose.yml +changes.txt + +!README.md +!CLA.md +!CONTRIBUTING.md +!.github/CLA-SETUP.md +!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/ + +# Frontend test tooling (Vitest) +node_modules/ +package-lock.json diff --git a/CLA.md b/CLA.md new file mode 100644 index 00000000..e107481c --- /dev/null +++ b/CLA.md @@ -0,0 +1,83 @@ +# Speakr Contributor License Agreement + +Thank you for your interest in contributing to Speakr ("We" or "Us"). + +This contributor agreement ("Agreement") documents the rights granted by contributors to Us. To make this document effective, please follow the instructions at [CONTRIBUTING.md](CONTRIBUTING.md). + +This is a legally binding document, so please read it carefully before agreeing to it. The Agreement may cover more than one software project managed by Us. + +## 1. Definitions + +"You" means the individual who Submits a Contribution to Us. + +"Contribution" means any work of authorship that is Submitted by You to Us in which You own or assert ownership of the Copyright. + +"Copyright" means all rights protecting works of authorship owned or controlled by You, including copyright, moral and neighboring rights, as appropriate, for the full term of their existence including any extensions by You. + +"Material" means the work of authorship which is made available by Us to third parties. + +"Submit" means any form of electronic, verbal, or written communication sent to Us or our representatives, including but not limited to electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, Us for the purpose of discussing and improving the Material, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." + +## 2. Grant of Rights + +### 2.1 Copyright License + +(a) You retain ownership of the Copyright in Your Contribution and have the same rights to use or license the Contribution which You would have had without entering into the Agreement. + +(b) To the maximum extent permitted by the relevant law, You grant to Us a perpetual, worldwide, non-exclusive, transferable, royalty-free, irrevocable license under the Copyright covering the Contribution, with the right to sublicense such rights through multiple tiers of sublicensees, to reproduce, modify, display, perform and distribute the Contribution as part of the Material; provided that this license is conditioned upon compliance with Section 2.3. + +### 2.2 Patent License + +For patent claims including, without limitation, method, process, and apparatus claims which You own, control or have the right to grant, now or in the future, You grant to Us a perpetual, worldwide, non-exclusive, transferable, royalty-free, irrevocable patent license, with the right to sublicense these rights to multiple tiers of sublicensees, to make, have made, use, sell, offer for sale, import and otherwise transfer the Contribution and the Contribution in combination with the Material (and portions of such combination). This license is granted only to the extent that the exercise of the licensed rights infringes such patent claims; and provided that this license is conditioned upon compliance with Section 2.3. + +### 2.3 Outbound License + +Based on the grant of rights in Sections 2.1 and 2.2, if We include Your Contribution in a Material, We may license the Contribution under any license, including copyleft, permissive, commercial, or proprietary licenses. As a condition on the exercise of this right, We agree to also license the Contribution under the terms of the license or licenses which We are using for the Material on the Submission Date. + +### 2.4 Moral Rights + +If moral rights apply to the Contribution, to the maximum extent permitted by law, You waive and agree not to assert such moral rights against Us or our successors in interest, or any of our licensees, either direct or indirect. + +### 2.5 Our Rights + +You acknowledge that We are not obligated to use Your Contribution as part of the Material and may decide to include any Contribution We consider appropriate. + +### 2.6 Reservation of Rights + +Any rights not expressly licensed under this section are expressly reserved by You. + +## 3. Agreement + +You confirm that: + +(a) You have the legal authority to enter into this Agreement. + +(b) You own the Copyright and patent claims covering the Contribution which are required to grant the rights under Section 2. + +(c) The grant of rights under Section 2 does not violate any grant of rights which You have made to third parties, including Your employer. If You are an employee, You have had Your employer approve this Agreement or sign the Entity version of this document. If You are less than eighteen years old, please have Your parents or guardian sign the Agreement. + +(d) You have followed the instructions in [CONTRIBUTING.md](CONTRIBUTING.md), if You do not own the Copyright in the entire work of authorship Submitted. + +## 4. Disclaimer + +EXCEPT FOR THE EXPRESS WARRANTIES IN SECTION 3, THE CONTRIBUTION IS PROVIDED "AS IS". MORE PARTICULARLY, ALL EXPRESS OR IMPLIED WARRANTIES INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT ARE EXPRESSLY DISCLAIMED BY YOU TO US. TO THE EXTENT THAT ANY SUCH WARRANTIES CANNOT BE DISCLAIMED, SUCH WARRANTY IS LIMITED IN DURATION TO THE MINIMUM PERIOD PERMITTED BY LAW. + +## 5. Consequential Damage Waiver + +TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT WILL YOU BE LIABLE FOR ANY LOSS OF PROFITS, LOSS OF ANTICIPATED SAVINGS, LOSS OF DATA, INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL AND EXEMPLARY DAMAGES ARISING OUT OF THIS AGREEMENT REGARDLESS OF THE LEGAL OR EQUITABLE THEORY (CONTRACT, TORT OR OTHERWISE) UPON WHICH THE CLAIM IS BASED. + +## 6. Miscellaneous + +6.1 This Agreement will be governed by and construed in accordance with the laws of the jurisdiction in which the copyright holder primarily resides, excluding its conflicts of law provisions. + +6.2 This Agreement sets out the entire agreement between You and Us for Your Contributions to Us and overrides all other agreements or understandings. + +6.3 If You or We assign the rights or obligations received through this Agreement to a third party, as a condition of the assignment, that third party must agree in writing to abide by all the rights and obligations in the Agreement. + +6.4 The failure of either party to require performance by the other party of any provision of this Agreement in one situation shall not affect the right of a party to require such performance at any time in the future. A waiver of performance under a provision in one situation shall not be considered a waiver of the performance of the provision in the future or a waiver of the provision in its entirety. + +6.5 If any provision of this Agreement is found void and unenforceable, such provision will be replaced to the extent possible with a provision that comes closest to the meaning of the original provision and which is enforceable. The terms and conditions set forth in this Agreement shall apply notwithstanding any failure of essential purpose of this Agreement or any limited remedy to the maximum extent possible under law. + +--- + +**By signing this agreement, you grant the Speakr project maintainers the rights described above, which allows us to maintain dual licensing (AGPLv3 and Commercial) while ensuring your valuable contributions remain part of the project.** diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..271d3d4d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,150 @@ +# Contributing to Speakr + +Thank you for your interest in contributing to Speakr! We appreciate your time and effort in helping improve this project. + +## Ways to Contribute + +There are many ways to contribute to Speakr: + +- **Report Bugs**: [Open an issue](https://github.com/murtaza-nasir/speakr/issues) describing the problem +- **Suggest Features**: [Start a discussion](https://github.com/murtaza-nasir/speakr/discussions) about your idea +- **Improve Documentation**: Help us make our docs clearer and more comprehensive +- **Translate**: Help translate Speakr into more languages +- **Sponsor**: Support the project financially to enable continued development + +## Code Contributions + +We welcome code contributions! However, due to the dual-licensing nature of Speakr (AGPLv3 and Commercial), all code contributions are subject to our Contributor License Agreement (CLA). + +### Contributor License Agreement (CLA) + +Speakr is dual-licensed under: +1. **AGPLv3** - Open source license for the community +2. **Commercial License** - For organizations that cannot comply with AGPLv3 + +The CLA allows us to: +- Accept your valuable contributions +- Include them in both the open source and commercial versions +- Maintain flexibility to update licenses if needed in the future +- Protect the project from legal issues + +**Important**: You retain copyright ownership of your contribution. The CLA simply grants us permission to use it. + +### Accepting the CLA + +**By submitting a pull request to this repository, you agree to the terms of our [Contributor License Agreement](CLA.md).** + +Please review the [CLA document](CLA.md) before submitting your contribution. When you open a PR, our bot will post a reminder about the CLA terms. + +### Contribution Process + +1. **Fork** the repository +2. **Create a branch** for your feature: `git checkout -b feature/my-awesome-feature` +3. **Make your changes** following our coding standards +4. **Test your changes** thoroughly +5. **Commit** with clear, descriptive messages (see our commit policy below) +6. **Push** to your fork: `git push origin feature/my-awesome-feature` +7. **Open a Pull Request** with a clear description of your changes +8. **Respond to feedback** from maintainers + +### Coding Standards + +- Follow the existing code style (Python PEP 8 for backend, Vue 3 conventions for frontend) +- Write clear, descriptive commit messages (see below) +- Include comments for complex logic +- Test your changes before submitting +- Keep PRs focused on a single feature or fix + +### Commit Message Guidelines + +Follow the format used in the project: + +``` +Brief description of what was done + +Optional longer explanation if needed +``` + +**Good examples:** +- `Add inline transcript editing in speaker identification modal` +- `Fix undefined handle_openai_api_error function call in summary error handler` +- `Optimize recording view for mobile with compact layout` + +**Avoid:** +- `Fixed bug` +- `Update` +- `Changes` + +### Pull Request Guidelines + +- Keep PRs focused on a single feature or bug fix +- Reference related issues: `Fixes #123` or `Relates to #456` +- Provide clear description of what changed and why +- Include screenshots for UI changes +- Ensure all tests pass (if applicable) +- Be responsive to review feedback + +## Development Setup + +See [CLAUDE.md](CLAUDE.md) for detailed development setup instructions. + +### Quick Start + +```bash +# Clone your fork +git clone https://github.com/YOUR-USERNAME/speakr.git +cd speakr + +# Set up development environment +docker-compose -f docker-compose.dev.yml up -d --build + +# Or for local development +python -m venv venv +source venv/bin/activate # On Windows: venv\Scripts\activate +pip install -r requirements.txt +python src/app.py --debug +``` + +## What Happens After You Submit a PR? + +1. **CLA Reminder**: Our bot posts a reminder about the CLA terms (by submitting, you've accepted them) +2. **Automated Tests**: CI/CD pipeline runs (if configured) +3. **Code Review**: Maintainers review your code +4. **Feedback**: You may be asked to make changes +5. **Merge**: Once approved, we merge your PR! + +## Other Ways to Help + +There are many ways to contribute without code: + +- **Bug Reports**: Detailed bug reports are incredibly valuable +- **Feature Requests**: Share your ideas and use cases +- **Documentation**: Typo fixes, clarifications, examples +- **Translations**: Help translate the UI +- **Community Support**: Help others in discussions and issues +- **Spread the Word**: Blog posts, social media, talks about Speakr + +## Questions? + +- **General Questions**: [GitHub Discussions](https://github.com/murtaza-nasir/speakr/discussions) +- **Bug Reports**: [GitHub Issues](https://github.com/murtaza-nasir/speakr/issues) + +## Code of Conduct + +Be respectful, inclusive, and professional. We're all here to build something great together. + +- Be kind and courteous +- Respect differing viewpoints +- Accept constructive criticism gracefully +- Focus on what's best for the community +- Show empathy towards others + +Violations may result in being blocked from contributing. + +## License + +By contributing to Speakr, you agree that your contributions will be licensed under the project's dual-license model (AGPLv3 and Commercial), as specified in the [CLA](CLA.md). + +--- + +**Thank you for contributing to Speakr!** 🎉 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..b91bae3a --- /dev/null +++ b/Dockerfile @@ -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=0 + +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"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..0ad25db4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + 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. + + + Copyright (C) + + 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 . + +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 +. diff --git a/README.md b/README.md new file mode 100644 index 00000000..9f9dad17 --- /dev/null +++ b/README.md @@ -0,0 +1,393 @@ +
+ Speakr Logo +
+ +

Speakr

+

Self-hosted AI transcription and intelligent note-taking platform

+ +

+ AGPL v3 + Docker Build + Docker Pulls + Latest Version +

+ +

+ Documentation • + Quick Start • + Screenshots • + Docker Hub • + Releases +

+ +--- + +## Overview + +Speakr transforms your audio recordings into organized, searchable, and intelligent notes. Built for privacy-conscious groups and individuals, it runs entirely on your own infrastructure, ensuring your sensitive conversations remain completely private. + +
+ Speakr Main Interface +
+ +## Key Features + +### Core Functionality +- **Smart Recording & Upload** - Record directly in browser or upload existing audio files +- **AI Transcription** - High-accuracy transcription with speaker identification +- **Voice Profiles** - AI-powered speaker recognition with voice embeddings (requires WhisperX ASR service) +- **REST API v1** - Complete API with Swagger UI for automation tools (n8n, Zapier, Make) and dashboard widgets +- **Single Sign-On** - Authenticate with any OIDC provider (Keycloak, Azure AD, Google, Auth0, Pocket ID) +- **Audio-Transcript Sync** - Click transcript to jump to audio, auto-highlight current text, follow mode for hands-free playback +- **Interactive Chat** - Ask questions about your recordings and get AI-powered answers +- **Inquire Mode** - Semantic search across all recordings using natural language +- **Internationalization** - Full support for English, Spanish, French, German, Chinese, and Russian +- **Beautiful Themes** - Light and dark modes with customizable color schemes + +### Collaboration & Sharing +- **Internal Sharing** - Share recordings with specific users with granular permissions (view/edit/reshare) +- **Group Management** - Create groups with automatic sharing via group-scoped tags +- **Public Sharing** - Generate secure links to share recordings externally (admin-controlled) +- **Group Tags** - Tags that automatically share recordings with all group members + +### Organization & Management +- **Smart Tagging** - Organize with tags that include custom AI prompts and ASR settings +- **Tag Prompt Stacking** - Combine multiple tags to layer AI instructions for powerful transformations +- **Tag Protection** - Prevent specific recordings from being auto-deleted +- **Group Retention Policies** - Set custom retention periods per group tag +- **Auto-Deletion** - Automatic cleanup of old recordings with flexible retention policies + +## Real-World Use Cases + +Different people use Speakr's collaboration and retention features in different ways: + +| Use Case | Setup | What It Does | +|----------|-------|-------------| +| **Family memories** | Create "Family" group with protected tag | Everyone gets access to trips and events automatically, recordings preserved forever | +| **Book club discussions** | "Book Club" group, tag monthly meetings | All members auto-share discussions, can add personal notes about what resonated | +| **Work project group** | Share individually with 3 teammates | Temporary collaboration, easy to revoke when project ends | +| **Daily group standups** | Group tag with 14-day retention | Auto-share with group, auto-cleanup of routine meetings | +| **Architecture decisions** | Engineering group tag, protected from deletion | Technical discussions automatically shared, preserved permanently as reference | +| **Client consultations** | Individual share with view-only permission | Controlled external access, clients can't accidentally edit | +| **Research interviews** | Protected tag + Obsidian export | Preserve recordings indefinitely, transcripts auto-import to note-taking system | +| **Legal consultations** | Group tag with 7-year retention | Automatic sharing with legal group, compliance-based retention | +| **Sales calls** | Group tag with 1-year retention | Whole sales group learns from each call, cleanup after sales cycle | + +### Creative Tag Prompt Examples + +Tags with custom prompts transform raw recordings into exactly what you need: + +- **Recipe recordings**: Record yourself cooking while narrating - tag with "Recipe" to convert messy speech into formatted recipes with ingredient lists and numbered steps +- **Lecture notes**: Students tag lectures with "Study Notes" to get organized outlines with concepts, examples, and definitions instead of raw transcripts +- **Code reviews**: "Code Review" tag extracts issues, suggested changes, and action items in technical language developers can use directly +- **Meeting summaries**: "Action Items" tag ignores discussion and returns just decisions, tasks, and deadlines + +### Tag Stacking for Combined Effects + +Stack multiple tags to layer instructions: +- "Recipe" + "Gluten Free" = Formatted recipe with gluten substitution suggestions +- "Lecture" + "Biology 301" = Study notes format focused on biological terminology +- "Client Meeting" + "Legal Review" = Client requirements plus legal implications highlighted + +The order can matter - start with format tags, then add focus tags for best results. + +### Integration Examples + +- **Obsidian/Logseq**: Enable auto-export to write completed transcripts directly to your vault using your custom template - no manual export needed +- **Documentation wikis**: Map auto-export to your wiki's import folder for seamless transcript publishing +- **Content creation**: Create SRT subtitle templates from your audio recordings for podcasts or video content +- **Project management**: Extract action items with custom tag prompts, then auto-export for automated task creation + +## Quick Start + +### Using Docker (Recommended) + +```bash +# Create project directory +mkdir speakr && cd speakr + +# Download docker-compose configuration: +wget https://raw.githubusercontent.com/murtaza-nasir/speakr/master/config/docker-compose.example.yml -O docker-compose.yml + +# Download the environment template: +wget https://raw.githubusercontent.com/murtaza-nasir/speakr/master/config/env.transcription.example -O .env + +# Configure your API keys and launch +nano .env +docker compose up -d + +# Access at http://localhost:8899 +``` + +> **Lightweight image:** Use `learnedmachine/speakr:lite` for a smaller image (~725MB vs ~4.4GB) that skips PyTorch. All features work normally — only Inquire Mode's semantic search falls back to basic text search. + +**Required API Keys:** +- `TRANSCRIPTION_API_KEY` - For speech-to-text (OpenAI) or `ASR_BASE_URL` for self-hosted +- `TEXT_MODEL_API_KEY` - For summaries, titles, and chat (OpenRouter or OpenAI) + +### Transcription Options + +Speakr uses a **connector-based architecture** that auto-detects your transcription provider: + +| Option | Setup | Speaker Diarization | Voice Profiles | +|--------|-------|---------------------|----------------| +| **OpenAI Transcribe** | Just API key | ✅ `gpt-4o-transcribe-diarize` | ❌ | +| **WhisperX ASR** | GPU container | ✅ Best quality | ✅ | +| **Mistral Voxtral** | Just API key | ✅ Built-in | ❌ | +| **VibeVoice ASR** | Self-hosted (vLLM) | ✅ Built-in | ❌ | +| **Legacy Whisper** | Just API key | ❌ | ❌ | + +**Simplest setup (OpenAI with diarization):** +```bash +TRANSCRIPTION_API_KEY=sk-your-openai-key +TRANSCRIPTION_MODEL=gpt-4o-transcribe-diarize +``` + +**Best quality (Self-hosted WhisperX):** +```bash +ASR_BASE_URL=http://whisperx-asr:9000 +ASR_RETURN_SPEAKER_EMBEDDINGS=true # Enable voice profiles +``` +Requires [WhisperX ASR Service](https://github.com/murtaza-nasir/whisperx-asr-service) container with GPU. + +**Mistral Voxtral (cloud diarization):** +```bash +TRANSCRIPTION_CONNECTOR=mistral +TRANSCRIPTION_API_KEY=your-mistral-key +TRANSCRIPTION_MODEL=voxtral-mini-latest +``` + +**VibeVoice ASR (self-hosted, no cloud dependency):** +```bash +TRANSCRIPTION_CONNECTOR=vibevoice +TRANSCRIPTION_BASE_URL=http://your-vllm-server:8000 +TRANSCRIPTION_MODEL=vibevoice +``` +Requires [VibeVoice](https://huggingface.co/microsoft/VibeVoice-ASR) served via vLLM with GPU. + +> **⚠️ PyTorch 2.6 Users:** If you encounter a "Weights only load failed" error with WhisperX, add `TORCH_FORCE_NO_WEIGHTS_ONLY_LOAD=true` to your ASR container. See [troubleshooting](https://murtaza-nasir.github.io/speakr/troubleshooting#pytorch-26-weights-loading-error-whisperx-asr-service) for details. + +**[View Full Installation Guide →](https://murtaza-nasir.github.io/speakr/getting-started/installation)** + +## Documentation + +Complete documentation is available at **[murtaza-nasir.github.io/speakr](https://murtaza-nasir.github.io/speakr)** + +- [Getting Started](https://murtaza-nasir.github.io/speakr/getting-started) - Quick setup guide +- [User Guide](https://murtaza-nasir.github.io/speakr/user-guide/) - Learn all features +- [Admin Guide](https://murtaza-nasir.github.io/speakr/admin-guide/) - Administration and configuration +- [Troubleshooting](https://murtaza-nasir.github.io/speakr/troubleshooting) - Common issues and solutions +- [FAQ](https://murtaza-nasir.github.io/speakr/faq) - Frequently asked questions + +## Latest Release (v0.8.20-alpha) + +**Security: open-redirect fix in `is_safe_url` (CWE-601).** Patch release on top of v0.8.19-alpha. + +- The `is_safe_url()` helper validated `urljoin(request.host_url, target)` while `redirect()` was called with the raw `target`. A scheme-relative input such as `////evil.com` resolved to a same-host URL during validation but was emitted verbatim in the `Location` header, where browsers interpret it as a network-path-relative redirect to an attacker-controlled host. +- `is_safe_url()` now validates the raw target against a local-path allowlist: leading `/` required, scheme-relative URLs (`//`, `/\`), backslashes, control characters, and any value with a scheme or netloc are rejected. The duplicate copy in `src/api/auth.py` was removed; password login and the SSO `next` / callback flow share one validator. +- Reported by **RacerZ and Fushuling**. Tracked as a GitHub Security Advisory; CVE pending. Users on v0.8.19-alpha or earlier should upgrade promptly. + +No new features, no breaking changes. + +### Previous Release (v0.8.19-alpha) + +**Inquire-mode performance and re-embed reliability.** Patch release on top of v0.8.18-alpha. Vectorised chunk similarity search (60s → 2-3s on large libraries), embedding API retries with backoff, transactional rollback when a partial embedding response would otherwise drop chunks, and Re-embed all retry passes that include stale-chunk recordings regardless of status. + +### Previous Release (v0.8.18-alpha) + +**API v1 folder operations.** Patch release on top of v0.8.17-alpha (#274 follow-up). + +- `GET /api/v1/recordings?folder_id=` (or `?folder_id=none`) filters list responses by folder +- `PATCH /api/v1/recordings/{id}` accepts `folder_id` to move a recording (or `null` to remove it from any folder) +- `PATCH /api/v1/recordings/batch` accepts `folder_id` inside `updates` for bulk moves +- OpenAPI schema documents all of the above plus the previously-undocumented batch fields (`is_inbox`, `is_highlighted`, `add_tag_ids`, `remove_tag_ids`) + +No breaking changes. The folder *resource* endpoints (CRUD on `/api/v1/folders`) shipped in v0.8.16-alpha; this release lets recordings actually be moved into and out of those folders. + +### Previous Release (v0.8.17-alpha) + +**Bug fixes and CI maintenance.** Patch release on top of v0.8.16-alpha. + +- Reprocess summary modal: prompt-variables panel and Append/Replace toggle now reflect the prompt source the user actually picked (was showing the recording's original tag variables and offering Append/Replace for tag-source prompts where it does not apply) +- Docs: corrected reverse-proxy nginx example so the WebSocket `Connection: upgrade` header is forwarded conditionally rather than set unconditionally (caused 500s on file uploads through the proxy with Gunicorn). Added a Nginx Proxy Manager section noting that NPM's default `client_max_body_size` is `2000m` and that the `Advanced` tab is the right place for per-host overrides. +- CI: bumped all GitHub Actions to Node 24 versions to clear deprecation warnings. + +No new features, no breaking changes. + +### Previous Release (v0.8.16-alpha) + +**Prompt Templating, Transcription UX Polish, Per-Recording Model Selection, and Observability** + +**Prompt templating and summary control** + +- **Prompt Template Variables** - Tag, folder, user-default, and admin-default summary prompts can contain `{{name}}` placeholders. Selecting a tag with `{{agenda}}` exposes an agenda input on the upload form; the value is stored on the recording, substituted into the prompt at summarisation time, and remains editable from the reprocess summary modal. Caps: 8,000 chars per value, 32,000 total. Single-pass `re.sub` substitution so values cannot introduce new placeholders or reach Python attributes. +- **Append vs Replace Mode** - The reprocess summary modal and the new "Customise summary prompt" modal each let you Append text to the resolved prompt (combine your saved prompt with extra context) or Replace it entirely (use only the text you paste). Append mode runs variable substitution after the append step so appended text can use the same `{{var}}` placeholders. +- **Customise Summary Prompt Split-Button** - A new control next to **Generate Summary** opens the Append/Replace modal for recordings that don't have a summary yet, so one-off context (an agenda, custom focus instructions) can be passed in without rewriting your saved prompt. +- **Full LLM Prompt Structure Preview** - Both the admin Default Prompts page and the user Customise-prompts tab now show the complete two-message payload (system prompt with context block, user message with transcription wrapper and language directive). Placeholder chips colour-code system tokens (blue, replaced by the framework) versus user-supplied variables (amber). The user-side preview re-renders live as you type into your custom prompt. + +**Per-recording transcription control** + +- **Per-Upload / Per-Tag / Per-Folder Transcription Model** - Set `TRANSCRIPTION_MODELS_AVAILABLE` and the upload form, reprocess modal, and tag/folder edit forms all gain a model dropdown. Tag and folder edit forms warn if a previously-selected default is no longer in the configured list. The dropdown is hidden when only one option would be visible. +- **Admin-Managed Transcription Model List** - When the connector exposes `/v1/models` discovery, admins can curate the list from the dashboard rather than via env var. Stored in the database; overrides `TRANSCRIPTION_MODELS_AVAILABLE` when set. +- **Per-Connector Capability Gating** - The hotwords, initial-prompt, and speaker-count UI elements are now hidden for connectors that don't support them, instead of accepting input that is silently ignored. +- **Mistral Voxtral Chunking** - `MISTRAL_ENABLE_CHUNKING=true` (with `MISTRAL_MAX_DURATION_SECONDS`) opts the Mistral connector into app-side chunking for recordings approaching Voxtral's 3-hour timeout. + +**ASR transcript editor** + +- **Autosave** - Saves edits 2 seconds after the last keystroke when the user opts in (`Account → Preferences → Autosave editor`). +- **Save Without Closing + Ctrl+S** - New button keeps the editor open after saving; Ctrl+S triggers a save from anywhere in the editor. +- **Scroll Memory** - Reopening the editor restores the previous scroll position instead of jumping to the top. +- **Double-Click to Edit** - Double-clicking a transcript row in the simple view jumps into the editor with that segment highlighted. +- **Row Highlight After Jump** - Briefly tints the row when navigating into it from the simple view so the target is obvious. + +**Account preferences** + +- **Preferences Tab** - Account settings has a new **Preferences** tab (split from the language settings) using a two-column layout for transcript display, editor behaviour, and language preferences. +- **Compact Timestamps** - Optional `mm:ss` (or `h:mm:ss`) timestamps in the simple transcript view, rendered as a two-part pill alongside the speaker label. The leading segment shows "Start" instead of `00:00`. +- **Persist Recording-List Sort** - The Created date / Meeting date toggle now sticks across reloads and sessions on the same browser (#263). + +**Embeddings and inquire mode** + +- **Configurable Embedding Model** - `EMBEDDING_MODEL` swaps `all-MiniLM-L6-v2` for any sentence-transformers model. +- **API-Mode Embeddings** - `EMBEDDING_BASE_URL`, `EMBEDDING_API_KEY`, and `EMBEDDING_DIMENSIONS` route embeddings through any OpenAI-compatible provider (vLLM, OpenRouter, OpenAI, Together, etc.). Inquire startup banner reflects the active provider. +- **Embedding Token Tracking + Re-Embed-All** - The Vector Store admin tab now tracks embedding API token usage and cost separately from LLM usage, and exposes a "Re-embed all" action for after a model or dimensionality change. Speakr warns at startup if the embedding identifier changed since data was stored. + +**Observability and admin** + +- **Per-Operation Token Stats** - Admin token statistics now break out title, summary, chat, event extraction, and embeddings as separate categories with their own cards and charts. Embedding usage is shown as a distinct cost line. +- **Granular Token Budgets** - `TITLE_MAX_TOKENS` and `EVENT_MAX_TOKENS` join the existing `SUMMARY_MAX_TOKENS` / `CHAT_MAX_TOKENS` so reasoning models that consume budget on hidden thinking tokens can be tuned per operation. The resolved `max_tokens` is logged with each LLM call. +- **LLM Timeout Visibility** - The configured `LLM_REQUEST_TIMEOUT` is logged at startup, and `APITimeoutError` log entries now include elapsed time so it is clear whether the timeout was the actual bound that fired. + +**API v1** + +- **Folder CRUD** - New `/api/v1/folders` endpoints for list, create, update, delete. +- **Connector Discovery** - New endpoint exposing the active transcription connector and its capabilities for companion-app integrations. +- **Recording Field Parity** - `/api/v1/recordings` and `/api/v1/recordings/{id}` now expose `audio_duration`, transcription/summarization durations, folder, events (detail only), `deletion_exempt`, `prompt_variables`, and the per-recording transcription model. +- **Forwarded Per-Request Overrides** - The `/api/v1/transcribe` endpoint now forwards `transcription_model`, `hotwords`, and `initial_prompt`. The custom-ASR-endpoint connector forwards a `?model=` query param so WhisperX runtime model switching works through the API. + +**Bug fixes** + +- Reprocessing now applies tag/folder/user default hotwords + initial_prompt (#265, previously only at upload time) +- Legacy user records with `transcription_language="français"` are normalised to ISO 639-1 codes on upgrade so WhisperX no longer 500s on display names (#256) +- Title generation no longer leaks `\\uXXXX` escape sequences into the LLM prompt for non-ASCII transcripts; truncation now happens after `format_transcription_for_llm` (#260) +- The Vector Store "recordings to process" message now uses the i18n params API instead of inline brace replace +- CSRF token added to the Preferences form so submissions are accepted + +**Infrastructure** + +- **Vitest Frontend Tests** - Pure-helper modules in `static/js/modules/utils/` are now covered by Vitest. Run `npm test`. Currently exercises the prompt-variable extraction and priority-chain logic. + +**Docs** + +- nginx reverse-proxy `proxy_request_buffering off` and `client_max_body_size` notes for large uploads +- Google Gemini OpenAI-compatible endpoint setup example +- Prompt template variables guide +- Per-upload / per-tag / per-folder model selection documentation +- `EMBEDDING_BASE_URL` API mode documentation across inquire-mode, vector-store, and troubleshooting + +--- + +**Older releases:** see the [GitHub Releases page](https://github.com/murtaza-nasir/speakr/releases) for tagged versions, or the [release history on the docs site](https://murtaza-nasir.github.io/speakr/#latest-updates) for narrative changelog entries going back to earlier v0.x lines. + +## Screenshots + + + + + + + + + + +
+ Main Screen +
Main Screen with Chat +
+ Video Playback +
Video Playback with Transcript +
+ Inquire Mode +
AI-Powered Semantic Search +
+ Transcription with Chat +
Interactive Transcription & Chat +
+ +**[View Full Screenshot Gallery →](https://murtaza-nasir.github.io/speakr/screenshots)** + +## Technology Stack + +- **Backend**: Python/Flask with SQLAlchemy +- **Frontend**: Vue.js 3 with Tailwind CSS +- **AI/ML**: OpenAI Whisper, OpenRouter, Ollama support +- **Database**: SQLite (default) or PostgreSQL +- **Deployment**: Docker, Docker Compose + +## Roadmap + +### Completed +- ✅ Speaker voice profiles with AI-powered identification (v0.5.9) +- ✅ Group workspaces with shared recordings (v0.5.9) +- ✅ PWA enhancements with offline support and background sync (v0.5.10) +- ✅ Multi-user job queue with fair scheduling (v0.6.0) +- ✅ SSO integration with OIDC providers (v0.7.0) +- ✅ Token usage tracking and per-user budgets (v0.7.2) +- ✅ Connector-based transcription architecture with auto-detection (v0.8.0) +- ✅ Comprehensive REST API with Swagger UI documentation (v0.8.0) +- ✅ Video retention with in-browser video playback (v0.8.11) +- ✅ Parallel uploads with duplicate detection (v0.8.11) +- ✅ Fullscreen video mode with live subtitles (v0.8.14) +- ✅ Custom vocabulary and transcription hints (v0.8.14) + +### Near-term +- Quick language switching for transcription +- Automated workflow triggers + +### Long-term +- Plugin system for custom integrations +- End-to-end encryption option + +### Reporting Issues + +- [Report bugs](https://github.com/murtaza-nasir/speakr/issues) +- [Request features](https://github.com/murtaza-nasir/speakr/discussions) + +## License + +This project is **dual-licensed**: + +1. **GNU Affero General Public License v3.0 (AGPLv3)** + [![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0) + + Speakr is offered under the AGPLv3 as its open-source license. You are free to use, modify, and distribute this software under the terms of the AGPLv3. A key condition of the AGPLv3 is that if you run a modified version on a network server and provide access to it for others, you must also make the source code of your modified version available to those users under the AGPLv3. + + * You **must** create a file named `LICENSE` (or `COPYING`) in the root of your repository and paste the full text of the [GNU AGPLv3 license](https://www.gnu.org/licenses/agpl-3.0.txt) into it. + * Read the full license text carefully to understand your rights and obligations. + +2. **Commercial License** + + For users or organizations who cannot or do not wish to comply with the terms of the AGPLv3 (for example, if you want to integrate Speakr into a proprietary commercial product or service without being obligated to share your modifications under AGPLv3), a separate commercial license is available. + + Please contact **speakr maintainers** for details on obtaining a commercial license. + +**You must choose one of these licenses** under which to use, modify, or distribute this software. If you are using or distributing the software without a commercial license agreement, you must adhere to the terms of the AGPLv3. + +## Contributing + +We welcome contributions to Speakr! There are many ways to help: + +- **Bug Reports & Feature Requests**: [Open an issue](https://github.com/murtaza-nasir/speakr/issues) +- **Discussions**: [Share ideas and ask questions](https://github.com/murtaza-nasir/speakr/discussions) +- **Documentation**: Help improve our docs +- **Translations**: Contribute translations for internationalization + +### Code Contributions + +By submitting a pull request, you agree to our [Contributor License Agreement (CLA)](CLA.md). This ensures we can maintain our dual-license model (AGPLv3 and Commercial). You retain copyright ownership of your contribution — the CLA simply grants us permission to include it in both the open source and commercial versions of Speakr. Our bot will post a reminder when you open a PR. + +**See our [Contributing Guide](CONTRIBUTING.md) for complete details on:** +- How the CLA works and why we need it +- Step-by-step contribution process +- Development setup instructions +- Coding standards and best practices diff --git a/VERSION b/VERSION new file mode 100644 index 00000000..cedaa4cb --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +v0.8.20-alpha diff --git a/app.ipynb b/app.ipynb deleted file mode 100644 index b1b26aab..00000000 --- a/app.ipynb +++ /dev/null @@ -1,243 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "I'll continue with the remaining JavaScript code and add the deployment instructions.\n", - "\n", - "\n", - "\n", - "```html\n", - "alert('Save failed: ' + error)\n", - " }\n", - " },\n", - " async loadRecordings() {\n", - " try {\n", - " const response = await fetch('/recordings')\n", - " const data = await response.json()\n", - " this.recordings = data\n", - " } catch (error) {\n", - " alert('Failed to load recordings: ' + error)\n", - " }\n", - " },\n", - " selectRecording(recording) {\n", - " this.selectedRecording = recording\n", - " }\n", - " },\n", - " mounted() {\n", - " this.loadRecordings()\n", - " }\n", - " }).mount('#app')\n", - " \n", - "\n", - "\n", - "\n", - "```\n", - "\n", - "Now, let's create a deployment script and requirements file for easy setup on Ubuntu:\n", - "\n", - "```text\n", - "# requirements.txt\n", - "flask==2.3.3\n", - "flask-sqlalchemy==3.1.1\n", - "openai==1.3.0\n", - "werkzeug==2.3.7\n", - "gunicorn==21.2.0\n", - "\n", - "# deployment/setup.sh\n", - "#!/bin/bash\n", - "\n", - "# Update system\n", - "sudo apt-get update\n", - "sudo apt-get upgrade -y\n", - "\n", - "# Install Python and pip if not already installed\n", - "sudo apt-get install -y python3 python3-pip python3-venv\n", - "\n", - "# Create directory for the application\n", - "sudo mkdir -p /opt/transcription-app\n", - "sudo chown $USER:$USER /opt/transcription-app\n", - "\n", - "# Create and activate virtual environment\n", - "python3 -m venv /opt/transcription-app/venv\n", - "source /opt/transcription-app/venv/bin/activate\n", - "\n", - "# Install requirements\n", - "pip install -r requirements.txt\n", - "\n", - "# Create systemd service file\n", - "sudo tee /etc/systemd/system/transcription.service << EOF\n", - "[Unit]\n", - "Description=Transcription Web Application\n", - "After=network.target\n", - "\n", - "[Service]\n", - "User=$USER\n", - "WorkingDirectory=/opt/transcription-app\n", - "Environment=\"PATH=/opt/transcription-app/venv/bin\"\n", - "ExecStart=/opt/transcription-app/venv/bin/gunicorn --workers 3 --bind 0.0.0.0:5000 app:app\n", - "\n", - "[Install]\n", - "WantedBy=multi-user.target\n", - "EOF\n", - "\n", - "# Create directories for uploads and database\n", - "mkdir -p /opt/transcription-app/uploads\n", - "mkdir -p /opt/transcription-app/instance\n", - "\n", - "# Reload systemd and start service\n", - "sudo systemctl daemon-reload\n", - "sudo systemctl start transcription\n", - "sudo systemctl enable transcription\n", - "\n", - "echo \"Installation complete! The application should be running on port 5000.\"\n", - "\n", - "```\n", - "\n", - "To deploy this application on Ubuntu:\n", - "\n", - "1. First, create a new directory for your project and copy all the files into it:\n", - " ```bash\n", - " mkdir transcription-app\n", - " cd transcription-app\n", - " ```\n", - "\n", - "2. Create the following directory structure:\n", - " ```\n", - " transcription-app/\n", - " ├── app.py\n", - " ├── requirements.txt\n", - " ├── deployment/\n", - " │ └── setup.sh\n", - " └── templates/\n", - " └── index.html\n", - " ```\n", - "\n", - "3. Make the setup script executable and run it:\n", - " ```bash\n", - " chmod +x deployment/setup.sh\n", - " ./deployment/setup.sh\n", - " ```\n", - "\n", - "The application will be installed as a system service and will:\n", - "- Run on port 5000\n", - "- Auto-start on system boot\n", - "- Use gunicorn for production-grade serving\n", - "- Store uploads in `/opt/transcription-app/uploads`\n", - "- Store the SQLite database in `/opt/transcription-app/instance`\n", - "\n", - "Key features of the application:\n", - "\n", - "1. Upload Page:\n", - " - Drag and drop interface for audio files\n", - " - Visual feedback during drag operations\n", - " - Automatic transition to input page after upload\n", - "\n", - "2. Input Page:\n", - " - Split view with transcription and metadata\n", - " - Auto-save functionality (saves 1 second after last edit)\n", - " - Manual save button for peace of mind\n", - " - Clean, modern interface\n", - "\n", - "3. Gallery Page:\n", - " - Time-based grouping (This Week, Last Week, Older)\n", - " - Audio playback functionality\n", - " - Detailed view of selected recording\n", - " - Easy navigation between recordings\n", - "\n", - "4. Database:\n", - " - SQLite for simplicity and portability\n", - " - Stores all metadata and file paths\n", - " - Automatic timestamps\n", - "\n", - "5. Security Features:\n", - " - Secure filename handling\n", - " - File size limits\n", - " - File type validation\n", - "\n", - "To customize the application, you can:\n", - "1. Modify the upload folder location in `app.py`\n", - "2. Adjust the maximum file size (currently 16MB)\n", - "3. Add additional metadata fields by updating both the database model and the frontend form\n", - "4. Customize the styling using Tailwind classes\n", - "5. Add user authentication if needed\n", - "\n", - "Would you like me to explain any particular part in more detail or make any modifications to the application?" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Here's how to run the app directly using Python:\n", - "\n", - "\n", - "\n", - "1. First, activate the virtual environment. If you used the setup script, it would be:\n", - "```bash\n", - "source /opt/transcription-app/venv/bin/activate\n", - "```\n", - "\n", - "You'll know the virtual environment is activated when you see `(venv)` at the start of your command prompt.\n", - "\n", - "2. Navigate to the application directory:\n", - "```bash\n", - "cd /opt/transcription-app\n", - "```\n", - "\n", - "3. Run the application directly with Python:\n", - "```bash\n", - "python app.py\n", - "```\n", - "\n", - "If you want to run it in development mode but on a different port:\n", - "```bash\n", - "export FLASK_ENV=development\n", - "export FLASK_DEBUG=1\n", - "python app.py\n", - "```\n", - "\n", - "Alternative way using Flask CLI:\n", - "```bash\n", - "export FLASK_APP=app.py\n", - "flask run --host=0.0.0.0 --port=8080\n", - "```\n", - "\n", - "To stop the application, press `Ctrl+C`.\n", - "\n", - "Some useful commands for virtual environment management:\n", - "```bash\n", - "# Deactivate the virtual environment when you're done\n", - "deactivate\n", - "\n", - "# If you need to install additional packages\n", - "pip install package_name\n", - "\n", - "# To see all installed packages\n", - "pip list\n", - "\n", - "# To update requirements.txt after installing new packages\n", - "pip freeze > requirements.txt\n", - "```\n", - "\n", - "If you're developing and want to see the logs in real-time:\n", - "```bash\n", - "# In development mode, logs will show directly in the terminal\n", - "# If you want to see more detailed logs, you can add this to app.py:\n", - "import logging\n", - "logging.basicConfig(level=logging.DEBUG)\n", - "```\n", - "\n", - "Would you like me to help you set up any specific development configuration?" - ] - } - ], - "metadata": { - "language_info": { - "name": "python" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/app.py b/app.py deleted file mode 100644 index 8eb86bba..00000000 --- a/app.py +++ /dev/null @@ -1,939 +0,0 @@ -# Speakr - Audio Transcription and Summarization App -import os -import sys -from flask import Flask, render_template, request, jsonify, send_file, Markup, redirect, url_for, flash -from flask_sqlalchemy import SQLAlchemy -from datetime import datetime -from openai import OpenAI # Keep using the OpenAI library -import json -from werkzeug.utils import secure_filename -from werkzeug.exceptions import RequestEntityTooLarge -from sqlalchemy import select -import threading -from dotenv import load_dotenv # Import load_dotenv -import httpx -import re -import markdown -from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user -from flask_bcrypt import Bcrypt -from flask_wtf import FlaskForm -from wtforms import StringField, PasswordField, SubmitField, BooleanField -from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationError - -# Load environment variables from .env file -load_dotenv() - -# Initialize Flask-Bcrypt -bcrypt = Bcrypt() - -# Helper function to convert markdown to HTML -def md_to_html(text): - if not text: - return "" - # Convert markdown to HTML with extensions for tables, code highlighting, etc. - html = markdown.markdown(text, extensions=[ - 'tables', # Support for tables - 'fenced_code', # Support for ```code blocks``` - 'codehilite', # Syntax highlighting for code blocks - 'nl2br', # Convert newlines to
tags - 'sane_lists', # Better list handling - 'smarty' # Smart quotes, dashes, etc. - ]) - return html - -app = Flask(__name__) -# Ensure the path uses the directory structure from your setup script -app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////opt/transcription-app/instance/transcriptions.db' -app.config['UPLOAD_FOLDER'] = '/opt/transcription-app/uploads' # Use absolute path based on setup -app.config['MAX_CONTENT_LENGTH'] = 250 * 1024 * 1024 # 250MB max file size -# Set a secret key for session management and CSRF protection -app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'default-dev-key-change-in-production') -db = SQLAlchemy() -db.init_app(app) - -# Initialize Flask-Login -login_manager = LoginManager() -login_manager.init_app(app) -login_manager.login_view = 'login' -login_manager.login_message_category = 'info' -bcrypt.init_app(app) - -# Add context processor to make 'now' available to all templates -@app.context_processor -def inject_now(): - return {'now': datetime.now()} - -# Ensure upload and instance directories exist -os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) -# Assuming the instance folder is handled correctly by Flask or created by setup.sh -# os.makedirs(os.path.dirname(app.config['SQLALCHEMY_DATABASE_URI'].replace('sqlite:///', '/')), exist_ok=True) - - -# --- User loader for Flask-Login --- -@login_manager.user_loader -def load_user(user_id): - return db.session.get(User, int(user_id)) - -# --- Database Models --- -class User(db.Model, UserMixin): - id = db.Column(db.Integer, primary_key=True) - username = db.Column(db.String(20), unique=True, nullable=False) - email = db.Column(db.String(120), unique=True, nullable=False) - password = db.Column(db.String(60), nullable=False) - is_admin = db.Column(db.Boolean, default=False) - recordings = db.relationship('Recording', backref='owner', lazy=True) - - def __repr__(self): - return f"User('{self.username}', '{self.email}')" -class Recording(db.Model): - # Add user_id foreign key to associate recordings with users - user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True) - id = db.Column(db.Integer, primary_key=True) - # Title will now often be AI-generated, maybe start with filename? - title = db.Column(db.String(200), nullable=True) # Allow Null initially - participants = db.Column(db.String(500)) - notes = db.Column(db.Text) - transcription = db.Column(db.Text, nullable=True) - summary = db.Column(db.Text, nullable=True) # <-- ADDED: Summary field - status = db.Column(db.String(50), default='PENDING') # PENDING, PROCESSING, SUMMARIZING, COMPLETED, FAILED - audio_path = db.Column(db.String(500)) - created_at = db.Column(db.DateTime, default=datetime.utcnow) - meeting_date = db.Column(db.Date, nullable=True) # <-- ADDED: Meeting Date field - file_size = db.Column(db.Integer) # Store file size in bytes - original_filename = db.Column(db.String(500), nullable=True) # Store the original uploaded filename - - def to_dict(self): - return { - 'id': self.id, - 'title': self.title, - 'participants': self.participants, - 'notes': self.notes, - 'notes_html': md_to_html(self.notes) if self.notes else "", - 'transcription': self.transcription, - 'summary': self.summary, - 'summary_html': md_to_html(self.summary) if self.summary else "", - 'status': self.status, - 'created_at': self.created_at.isoformat() if self.created_at else None, - 'meeting_date': self.meeting_date.isoformat() if self.meeting_date else None, # <-- ADDED: Include meeting_date - 'file_size': self.file_size, - 'original_filename': self.original_filename, # <-- ADDED: Include original filename - 'user_id': self.user_id - } - -# --- Forms for Authentication --- -class RegistrationForm(FlaskForm): - username = StringField('Username', validators=[DataRequired(), Length(min=2, max=20)]) - email = StringField('Email', validators=[DataRequired(), Email()]) - password = PasswordField('Password', validators=[DataRequired(), Length(min=8)]) - 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('Password', validators=[DataRequired()]) - remember = BooleanField('Remember Me') - submit = SubmitField('Login') - -with app.app_context(): - db.create_all() - -# --- API client setup for OpenRouter --- -# Use environment variables from .env -openrouter_api_key = os.environ.get("OPENROUTER_API_KEY") -openrouter_base_url = os.environ.get("OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1") -openrouter_model_name = os.environ.get("OPENROUTER_MODEL_NAME", "openai/gpt-3.5-turbo") # Default if not set - -http_client_no_proxy = httpx.Client(verify=True) # verify=True is default, but good to be explicit - -if not openrouter_api_key: - app.logger.warning("OPENROUTER_API_KEY not found. Title/Summary generation DISABLED.") -else: - try: - # ---> Pass the custom httpx_client <--- - client = OpenAI( - api_key=openrouter_api_key, - base_url=openrouter_base_url, - http_client=http_client_no_proxy # Pass the proxy-disabled client - ) - app.logger.info(f"OpenRouter client initialized. Using model: {openrouter_model_name}") - except Exception as client_init_e: - app.logger.error(f"Failed to initialize OpenRouter client: {client_init_e}", exc_info=True) - -# Store details for the transcription client (potentially different) -transcription_api_key = os.environ.get("TRANSCRIPTION_API_KEY", "cant-be-empty") -transcription_base_url = os.environ.get("TRANSCRIPTION_BASE_URL", "https://openrouter.ai/api/v1") - -app.logger.info(f"Using OpenRouter model for summaries: {openrouter_model_name}") - -# --- Background Transcription & Summarization Task --- -def transcribe_audio_task(app_context, recording_id, filepath, original_filename): - """Runs the transcription and summarization in a background thread.""" - with app_context: # Need app context for db operations in thread - recording = db.session.get(Recording, recording_id) - if not recording: - app.logger.error(f"Error: Recording {recording_id} not found for transcription.") - return - - try: - app.logger.info(f"Starting transcription for recording {recording_id} ({original_filename})...") - recording.status = 'PROCESSING' - db.session.commit() - - # --- Step 1: Transcription --- - with open(filepath, 'rb') as audio_file: - # NOTE: This still uses the hardcoded Whisper model. - # If you want OpenRouter for transcription too, change the model here - # and potentially adjust the API call if needed. - # For now, assuming Whisper via a local compatible endpoint. - # You might need a *separate* client for Whisper if it's at a different URL. - # Example using the configured client (assuming it points to Whisper or OpenRouter handles it): - transcription_client = OpenAI( - api_key=transcription_api_key, - base_url=transcription_base_url, - http_client=http_client_no_proxy # Reuse the same client configuration - ) - # Get the Whisper model name from environment variables - whisper_model = os.environ.get("WHISPER_MODEL", "Systran/faster-distil-whisper-large-v3") - transcript = transcription_client.audio.transcriptions.create( - model=whisper_model, # Use model from environment variables - file=audio_file, - language="en" # Specify language if known - ) - recording.transcription = transcript.text - app.logger.info(f"Transcription completed for recording {recording_id}. Text length: {len(recording.transcription)}") - # Don't commit yet, proceed to summarization - - # --- Step 2: Title & Summary Generation using OpenRouter --- - if client is None: # Check if OpenRouter client initialized successfully earlier - app.logger.warning(f"Skipping summary for {recording_id}: OpenRouter client not configured.") - recording.summary = "[Summary skipped: OpenRouter client not configured]" - recording.status = 'COMPLETED' - db.session.commit() - return # Exit cleanly - - recording.status = 'SUMMARIZING' # Update status - db.session.commit() - app.logger.info(f"Requesting title and summary from OpenRouter for recording {recording_id} using model {openrouter_model_name}...") - - if not recording.transcription or len(recording.transcription.strip()) < 10: # Basic check for valid transcript - app.logger.warning(f"Transcription for recording {recording_id} is too short or empty. Skipping summarization.") - recording.status = 'COMPLETED' # Mark as completed even without summary - recording.summary = "[Summary skipped due to short transcription]" - db.session.commit() - return # Exit the task cleanly - - # Prepare the prompt for OpenRouter - prompt_text = f"""Analyze the following audio transcription and generate a concise title and a brief summary. - -Transcription: -\"\"\" -{recording.transcription[:30000]} -\"\"\" - -Respond STRICTLY with a JSON object containing two keys: "title" (a short, descriptive title, max 15 words) and "summary" (a paragraph summarizing the key points, max 150 words). -Example Format: -{{ - "title": "Example Meeting Discussion on Q3 Results", - "summary": "The meeting covered the financial results for Q3, highlighting key achievements and areas for improvement. Action items were assigned for follow-up." -}} - -JSON Response:""" # The prompt guides the model towards the desired output - - try: - # Use the OpenRouter client configured earlier - completion = client.chat.completions.create( - model=openrouter_model_name, - messages=[ - {"role": "system", "content": "You are an AI assistant that generates titles and summaries for meeting transcripts. Respond only with the requested JSON object."}, - {"role": "user", "content": prompt_text} - ], - temperature=0.5, # Adjust temperature as needed - max_tokens=300, # Adjust based on expected title+summary length - response_format={"type": "json_object"} # Request JSON output - ) - - response_content = completion.choices[0].message.content - app.logger.debug(f"Raw OpenRouter response for {recording_id}: {response_content}") - - try: - response_content = completion.choices[0].message.content - app.logger.debug(f"Raw OpenRouter response for {recording_id}: {response_content}") - - # Use regex to extract JSON content from potential markdown code blocks - # This looks for content between markdown code blocks or just takes the whole content - json_match = re.search(r'```(?:json)?(.*?)```|(.+)', response_content, re.DOTALL) - - if json_match: - # Use the first group that matched (either between ``` or the whole content) - sanitized_response = json_match.group(1) if json_match.group(1) else json_match.group(2) - sanitized_response = sanitized_response.strip() - else: - sanitized_response = response_content.strip() - - summary_data = json.loads(response_content) - generated_title = summary_data.get("title") - generated_summary = summary_data.get("summary") - - if generated_title and generated_summary: - # Update recording with AI generated content - recording.title = generated_title.strip() - recording.summary = generated_summary.strip() - recording.status = 'COMPLETED' - app.logger.info(f"Title and summary generated successfully for recording {recording_id}.") - else: - app.logger.warning(f"OpenRouter response for {recording_id} lacked 'title' or 'summary' key. Response: {response_content}") - recording.summary = "[AI summary generation failed: Invalid JSON structure]" - recording.status = 'COMPLETED' # Still completed, but summary failed - - except json.JSONDecodeError as json_e: - app.logger.error(f"Failed to parse JSON response from OpenRouter for {recording_id}: {json_e}. Response: {response_content}") - recording.summary = f"[AI summary generation failed: Invalid JSON response ({json_e})]" - recording.status = 'COMPLETED' # Mark as completed, summary failed - - except Exception as summary_e: - app.logger.error(f"Error calling OpenRouter API for summary ({recording_id}): {str(summary_e)}") - # Keep transcription, but mark summary failed. Don't change status from SUMMARIZING yet. - recording.summary = f"[AI summary generation failed: API Error ({str(summary_e)})]" - recording.status = 'COMPLETED' # Even if summary fails, transcription worked. - - - db.session.commit() # Final commit for this step - - except Exception as e: - db.session.rollback() # Rollback if any step failed critically - app.logger.error(f"Processing FAILED for recording {recording_id}: {str(e)}", exc_info=True) - # Retrieve recording again in case session was rolled back - recording = db.session.get(Recording, recording_id) - if recording: - # Ensure status reflects failure even after rollback/retrieve attempt - if recording.status not in ['COMPLETED', 'FAILED']: # Avoid overwriting final state - recording.status = 'FAILED' - if not recording.transcription: # If transcription itself failed - recording.transcription = f"Processing failed: {str(e)}" - # Add error note to summary if appropriate stage was reached - if recording.status == 'SUMMARIZING' and not recording.summary: - recording.summary = f"[Processing failed during summarization: {str(e)}]" - - db.session.commit() - -# --- Chat with Transcription --- -@app.route('/chat', methods=['POST']) -@login_required -def chat_with_transcription(): - try: - data = request.json - if not data: - return jsonify({'error': 'No data provided'}), 400 - - recording_id = data.get('recording_id') - user_message = data.get('message') - message_history = data.get('message_history', []) - - if not recording_id: - return jsonify({'error': 'No recording ID provided'}), 400 - if not user_message: - return jsonify({'error': 'No message provided'}), 400 - - # Get the recording - recording = db.session.get(Recording, recording_id) - if not recording: - return jsonify({'error': 'Recording not found'}), 404 - - # Check if the recording belongs to the current user - if recording.user_id and recording.user_id != current_user.id: - return jsonify({'error': 'You do not have permission to chat with this recording'}), 403 - - # Check if OpenRouter client is available - if client is None: - return jsonify({'error': 'Chat service is not available (OpenRouter client not configured)'}), 503 - - # Prepare the system prompt with the transcription - system_prompt = f"""You are a professional meeting analyst working with Murtaza Nasir, Assistant Professor at Wichita State University. Analyze the following meeting information and respond to the specific request. - -Following are the meeting participants and their roles: -{recording.participants or "No specific participants information provided."} - -Following is the meeting transcript: -<> -{recording.transcription or "No transcript available."} -<> - -Additional context and notes about the meeting: -{recording.notes or "none"} -""" - - # Call the LLM - try: - # Prepare messages array with system prompt and conversation history - messages = [{"role": "system", "content": system_prompt}] - - # Add message history if provided - if message_history: - messages.extend(message_history) - - # Add the current user message - messages.append({"role": "user", "content": user_message}) - - completion = client.chat.completions.create( - model=openrouter_model_name, - messages=messages, - temperature=0.7, - max_tokens=1000 - ) - - response_content = completion.choices[0].message.content - - # Convert markdown in the response to HTML - response_html = md_to_html(response_content) - - return jsonify({ - 'response': response_content, - 'response_html': response_html, - 'success': True - }) - - except Exception as chat_error: - app.logger.error(f"Error calling OpenRouter API for chat: {str(chat_error)}") - return jsonify({'error': f'Chat service error: {str(chat_error)}'}), 500 - - except Exception as e: - app.logger.error(f"Error in chat endpoint: {str(e)}") - return jsonify({'error': str(e)}), 500 - - -# --- Authentication Routes --- -@app.route('/register', methods=['GET', 'POST']) -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('login')) - - if current_user.is_authenticated: - return redirect(url_for('index')) - - form = RegistrationForm() - if form.validate_on_submit(): - hashed_password = bcrypt.generate_password_hash(form.password.data).decode('utf-8') - user = User(username=form.username.data, email=form.email.data, password=hashed_password) - db.session.add(user) - db.session.commit() - flash('Your account has been created! You can now log in.', 'success') - return redirect(url_for('login')) - - return render_template('register.html', title='Register', form=form) - -@app.route('/login', methods=['GET', 'POST']) -def login(): - if current_user.is_authenticated: - return redirect(url_for('index')) - - form = LoginForm() - if form.validate_on_submit(): - user = User.query.filter_by(email=form.email.data).first() - if user and bcrypt.check_password_hash(user.password, form.password.data): - login_user(user, remember=form.remember.data) - next_page = request.args.get('next') - return redirect(next_page) if next_page else redirect(url_for('index')) - else: - flash('Login unsuccessful. Please check email and password.', 'danger') - - return render_template('login.html', title='Login', form=form) - -@app.route('/logout') -def logout(): - logout_user() - return redirect(url_for('index')) - -@app.route('/account', methods=['GET']) -@login_required -def account(): - return render_template('account.html', title='Account') - -@app.route('/change_password', methods=['POST']) -@login_required -def change_password(): - current_password = request.form.get('current_password') - new_password = request.form.get('new_password') - confirm_password = request.form.get('confirm_password') - - # Validate form data - if not current_password or not new_password or not confirm_password: - flash('All fields are required.', 'danger') - return redirect(url_for('account')) - - if new_password != confirm_password: - flash('New password and confirmation do not match.', 'danger') - return redirect(url_for('account')) - - # Check if current password is correct - if not bcrypt.check_password_hash(current_user.password, current_password): - flash('Current password is incorrect.', 'danger') - return redirect(url_for('account')) - - # Update password - current_user.password = bcrypt.generate_password_hash(new_password).decode('utf-8') - db.session.commit() - - flash('Your password has been updated successfully.', 'success') - return redirect(url_for('account')) - -# --- Admin Routes --- -@app.route('/admin', methods=['GET']) -@login_required -def admin(): - # Check if user is admin - if not current_user.is_admin: - flash('You do not have permission to access the admin page.', 'danger') - return redirect(url_for('index')) - return render_template('admin.html', title='Admin Dashboard') - -@app.route('/admin/users', methods=['GET']) -@login_required -def admin_get_users(): - # Check if user is admin - if not current_user.is_admin: - return jsonify({'error': 'Unauthorized'}), 403 - - users = User.query.all() - user_data = [] - - for user in users: - # Get recordings count and storage used - recordings_count = len(user.recordings) - storage_used = sum(r.file_size for r in user.recordings if r.file_size) or 0 - - user_data.append({ - 'id': user.id, - 'username': user.username, - 'email': user.email, - 'is_admin': user.is_admin, - 'recordings_count': recordings_count, - 'storage_used': storage_used - }) - - return jsonify(user_data) - -@app.route('/admin/users', methods=['POST']) -@login_required -def admin_add_user(): - # Check if user is admin - if not current_user.is_admin: - return jsonify({'error': 'Unauthorized'}), 403 - - data = request.json - if not data: - return jsonify({'error': 'No data provided'}), 400 - - # Validate required fields - required_fields = ['username', 'email', 'password'] - for field in required_fields: - if field not in data: - return jsonify({'error': f'Missing required field: {field}'}), 400 - - # Check if username or email already exists - if User.query.filter_by(username=data['username']).first(): - return jsonify({'error': 'Username already exists'}), 400 - - if User.query.filter_by(email=data['email']).first(): - return jsonify({'error': 'Email already exists'}), 400 - - # Create new user - hashed_password = bcrypt.generate_password_hash(data['password']).decode('utf-8') - new_user = User( - username=data['username'], - email=data['email'], - password=hashed_password, - is_admin=data.get('is_admin', False) - ) - - db.session.add(new_user) - db.session.commit() - - return jsonify({ - 'id': new_user.id, - 'username': new_user.username, - 'email': new_user.email, - 'is_admin': new_user.is_admin, - 'recordings_count': 0, - 'storage_used': 0 - }), 201 - -@app.route('/admin/users/', methods=['PUT']) -@login_required -def admin_update_user(user_id): - # Check if user is admin - if not current_user.is_admin: - return jsonify({'error': 'Unauthorized'}), 403 - - user = db.session.get(User, user_id) - if not user: - return jsonify({'error': 'User not found'}), 404 - - data = request.json - if not data: - return jsonify({'error': 'No data provided'}), 400 - - # Update user fields - if 'username' in data and data['username'] != user.username: - # Check if username already exists - if User.query.filter_by(username=data['username']).first(): - return jsonify({'error': 'Username already exists'}), 400 - user.username = data['username'] - - if 'email' in data and data['email'] != user.email: - # Check if email already exists - if User.query.filter_by(email=data['email']).first(): - return jsonify({'error': 'Email already exists'}), 400 - user.email = data['email'] - - if 'password' in data and data['password']: - user.password = bcrypt.generate_password_hash(data['password']).decode('utf-8') - - if 'is_admin' in data: - user.is_admin = data['is_admin'] - - db.session.commit() - - # Get recordings count and storage used - recordings_count = len(user.recordings) - storage_used = sum(r.file_size for r in user.recordings if r.file_size) or 0 - - return jsonify({ - 'id': user.id, - 'username': user.username, - 'email': user.email, - 'is_admin': user.is_admin, - 'recordings_count': recordings_count, - 'storage_used': storage_used - }) - -@app.route('/admin/users/', methods=['DELETE']) -@login_required -def admin_delete_user(user_id): - # Check if user is admin - if not current_user.is_admin: - return jsonify({'error': 'Unauthorized'}), 403 - - # Prevent deleting self - if user_id == current_user.id: - return jsonify({'error': 'Cannot delete your own account'}), 400 - - user = db.session.get(User, user_id) - if not user: - return jsonify({'error': 'User not found'}), 404 - - # Delete user's recordings and audio files - for recording in user.recordings: - try: - if recording.audio_path and os.path.exists(recording.audio_path): - os.remove(recording.audio_path) - except Exception as e: - app.logger.error(f"Error deleting audio file {recording.audio_path}: {e}") - - # Delete user - db.session.delete(user) - db.session.commit() - - return jsonify({'success': True}) - -@app.route('/admin/users//toggle-admin', methods=['POST']) -@login_required -def admin_toggle_admin(user_id): - # Check if user is admin - if not current_user.is_admin: - return jsonify({'error': 'Unauthorized'}), 403 - - # Prevent changing own admin status - if user_id == current_user.id: - return jsonify({'error': 'Cannot change your own admin status'}), 400 - - user = db.session.get(User, user_id) - if not user: - return jsonify({'error': 'User not found'}), 404 - - # Toggle admin status - user.is_admin = not user.is_admin - db.session.commit() - - return jsonify({'success': True, 'is_admin': user.is_admin}) - -@app.route('/admin/stats', methods=['GET']) -@login_required -def admin_get_stats(): - # Check if user is admin - if not current_user.is_admin: - return jsonify({'error': 'Unauthorized'}), 403 - - # Get total users - total_users = User.query.count() - - # Get total recordings - total_recordings = Recording.query.count() - - # Get recordings by status - completed_recordings = Recording.query.filter_by(status='COMPLETED').count() - processing_recordings = Recording.query.filter(Recording.status.in_(['PROCESSING', 'SUMMARIZING'])).count() - pending_recordings = Recording.query.filter_by(status='PENDING').count() - failed_recordings = Recording.query.filter_by(status='FAILED').count() - - # Get total storage used - total_storage = db.session.query(db.func.sum(Recording.file_size)).scalar() or 0 - - # Get top users by storage - top_users_query = db.session.query( - User.id, - User.username, - db.func.count(Recording.id).label('recordings_count'), - db.func.sum(Recording.file_size).label('storage_used') - ).join(Recording, User.id == Recording.user_id, isouter=True) \ - .group_by(User.id) \ - .order_by(db.func.sum(Recording.file_size).desc()) \ - .limit(5) - - top_users = [] - for user_id, username, recordings_count, storage_used in top_users_query: - top_users.append({ - 'id': user_id, - 'username': username, - 'recordings_count': recordings_count or 0, - 'storage_used': storage_used or 0 - }) - - # Get total queries (chat requests) - # This is a placeholder - you would need to track this in your database - total_queries = 0 - - return jsonify({ - 'total_users': total_users, - 'total_recordings': total_recordings, - 'completed_recordings': completed_recordings, - 'processing_recordings': processing_recordings, - 'pending_recordings': pending_recordings, - 'failed_recordings': failed_recordings, - 'total_storage': total_storage, - 'top_users': top_users, - 'total_queries': total_queries - }) - -# --- Flask Routes --- -@app.route('/') -def index(): - return render_template('index.html') - -@app.route('/recordings', methods=['GET']) -def get_recordings(): - try: - # Check if user is logged in - if not current_user.is_authenticated: - return jsonify([]) # Return empty array if not logged in - - # Filter recordings by the current user - stmt = select(Recording).where(Recording.user_id == current_user.id).order_by(Recording.created_at.desc()) - recordings = db.session.execute(stmt).scalars().all() - return jsonify([recording.to_dict() for recording in recordings]) - except Exception as e: - app.logger.error(f"Error fetching recordings: {e}") - return jsonify({'error': str(e)}), 500 - -@app.route('/save', methods=['POST']) -@login_required -def save_metadata(): - try: - data = request.json - if not data: return jsonify({'error': 'No data provided'}), 400 - recording_id = data.get('id') - if not recording_id: return jsonify({'error': 'No recording ID provided'}), 400 - - recording = db.session.get(Recording, recording_id) - if not recording: return jsonify({'error': 'Recording not found'}), 404 - - # Check if the recording belongs to the current user - if recording.user_id and recording.user_id != current_user.id: - return jsonify({'error': 'You do not have permission to edit this recording'}), 403 - - # Update fields if provided - if 'title' in data: recording.title = data['title'] - if 'participants' in data: recording.participants = data['participants'] - if 'notes' in data: recording.notes = data['notes'] - if 'summary' in data: recording.summary = data['summary'] # <-- ADDED: Allow saving edited summary - if 'meeting_date' in data: - try: - # Attempt to parse date string (e.g., "YYYY-MM-DD") - date_str = data['meeting_date'] - if date_str: - recording.meeting_date = datetime.strptime(date_str, '%Y-%m-%d').date() - else: - recording.meeting_date = None # Allow clearing the date - except (ValueError, TypeError) as e: - app.logger.warning(f"Could not parse meeting_date '{data.get('meeting_date')}': {e}") - # Optionally return an error or just ignore the invalid date - # return jsonify({'error': f"Invalid date format for meeting_date. Use YYYY-MM-DD."}), 400 - - # Do not update transcription or status here - db.session.commit() - return jsonify({'success': True, 'recording': recording.to_dict()}) - - except Exception as e: - db.session.rollback() - app.logger.error(f"Error saving metadata for recording {recording_id}: {e}") - return jsonify({'error': str(e)}), 500 - - -@app.route('/upload', methods=['POST']) -@login_required -def upload_file(): - try: - if 'file' not in request.files: - return jsonify({'error': 'No file provided'}), 400 - - file = request.files['file'] - if file.filename == '': - return jsonify({'error': 'No file selected'}), 400 - - original_filename = file.filename # <-- ADDED: Capture original filename - safe_filename = secure_filename(original_filename) - # Ensure filepath uses the configured UPLOAD_FOLDER - filepath = os.path.join(app.config['UPLOAD_FOLDER'], f"{datetime.now().strftime('%Y%m%d%H%M%S')}_{safe_filename}") - - # Get file size before saving - file.seek(0, os.SEEK_END) - file_size = file.tell() - file.seek(0) - - # Check size limit again - if file_size > app.config['MAX_CONTENT_LENGTH']: - raise RequestEntityTooLarge() - - file.save(filepath) - app.logger.info(f"File saved to {filepath}") - - # Create initial database entry with PENDING status and filename as placeholder title - recording = Recording( - audio_path=filepath, - original_filename=original_filename, # <-- ADDED: Save original filename - # Use original filename (without path part) as initial title - title=f"Recording - {original_filename}", - file_size=file_size, - status='PENDING', # Explicitly set status - meeting_date=datetime.utcnow().date(), # <-- ADDED: Default meeting_date to today - user_id=current_user.id # Associate with the current user - ) - db.session.add(recording) - db.session.commit() - app.logger.info(f"Initial recording record created with ID: {recording.id}") - - # --- Start transcription & summarization in background thread --- - thread = threading.Thread( - target=transcribe_audio_task, - # Pass original filename for logging clarity - args=(app.app_context(), recording.id, filepath, original_filename) # Pass original_filename here too - ) - thread.start() - app.logger.info(f"Background processing thread started for recording ID: {recording.id}") - - # Return the initial recording data and ID immediately - return jsonify(recording.to_dict()), 202 # 202 Accepted - - except RequestEntityTooLarge: - max_size_mb = app.config['MAX_CONTENT_LENGTH'] / (1024 * 1024) - app.logger.warning(f"Upload failed: File too large (>{max_size_mb}MB)") - return jsonify({ - 'error': f'File too large. Maximum size is {max_size_mb:.0f} MB.', - 'max_size_mb': max_size_mb - }), 413 - except Exception as e: - db.session.rollback() # Rollback if initial save failed - app.logger.error(f"Error during file upload: {e}", exc_info=True) - return jsonify({'error': str(e)}), 500 - - -# Status Endpoint -@app.route('/status/', methods=['GET']) -@login_required -def get_status(recording_id): - """Endpoint to check the transcription/summarization status.""" - try: - recording = db.session.get(Recording, recording_id) - if not recording: - return jsonify({'error': 'Recording not found'}), 404 - - # Check if the recording belongs to the current user - if recording.user_id and recording.user_id != current_user.id: - return jsonify({'error': 'You do not have permission to view this recording'}), 403 - - return jsonify(recording.to_dict()) - except Exception as e: - app.logger.error(f"Error fetching status for recording {recording_id}: {e}") - return jsonify({'error': str(e)}), 500 - -# Get Audio Endpoint -@app.route('/audio/') -@login_required -def get_audio(recording_id): - try: - recording = db.session.get(Recording, recording_id) - if not recording or not recording.audio_path: - return jsonify({'error': 'Recording or audio file not found'}), 404 - - # Check if the recording belongs to the current user - if recording.user_id and recording.user_id != current_user.id: - return jsonify({'error': 'You do not have permission to access this audio file'}), 403 - if not os.path.exists(recording.audio_path): - app.logger.error(f"Audio file missing from server: {recording.audio_path}") - return jsonify({'error': 'Audio file missing from server'}), 404 - return send_file(recording.audio_path) - except Exception as e: - app.logger.error(f"Error serving audio for recording {recording_id}: {e}") - return jsonify({'error': str(e)}), 500 - -# Delete Recording Endpoint -@app.route('/recording/', methods=['DELETE']) -@login_required -def delete_recording(recording_id): - try: - recording = db.session.get(Recording, recording_id) - if not recording: - return jsonify({'error': 'Recording not found'}), 404 - - # Check if the recording belongs to the current user - if recording.user_id and recording.user_id != current_user.id: - return jsonify({'error': 'You do not have permission to delete this recording'}), 403 - - # Delete the audio file first - try: - if recording.audio_path and os.path.exists(recording.audio_path): - os.remove(recording.audio_path) - app.logger.info(f"Deleted audio file: {recording.audio_path}") - except Exception as e: - app.logger.error(f"Error deleting audio file {recording.audio_path}: {e}") - - # Delete the database record - db.session.delete(recording) - db.session.commit() - app.logger.info(f"Deleted recording record ID: {recording_id}") - - return jsonify({'success': True}) - except Exception as e: - db.session.rollback() - app.logger.error(f"Error deleting recording {recording_id}: {e}") - return jsonify({'error': str(e)}), 500 - - -if __name__ == '__main__': - # Consider using waitress or gunicorn for production - # waitress-serve --host 0.0.0.0 --port 8899 app:app - # For development: - app.run(host='0.0.0.0', port=8899, debug=True) # Set debug=False if thread issues arise diff --git a/app.py.bak b/app.py.bak deleted file mode 100644 index e8f17f61..00000000 --- a/app.py.bak +++ /dev/null @@ -1,519 +0,0 @@ -# Speakr - Audio Transcription and Summarization App -import os -import sys -from flask import Flask, render_template, request, jsonify, send_file, Markup -from flask_sqlalchemy import SQLAlchemy -from datetime import datetime -from openai import OpenAI # Keep using the OpenAI library -import json -from werkzeug.utils import secure_filename -from werkzeug.exceptions import RequestEntityTooLarge -from sqlalchemy import select -import threading -from dotenv import load_dotenv # Import load_dotenv -import httpx -import re -import markdown - -# Load environment variables from .env file -load_dotenv() - -# Helper function to convert markdown to HTML -def md_to_html(text): - if not text: - return "" - # Convert markdown to HTML with extensions for tables, code highlighting, etc. - html = markdown.markdown(text, extensions=[ - 'tables', # Support for tables - 'fenced_code', # Support for ```code blocks``` - 'codehilite', # Syntax highlighting for code blocks - 'nl2br', # Convert newlines to
tags - 'sane_lists', # Better list handling - 'smarty' # Smart quotes, dashes, etc. - ]) - return html - -app = Flask(__name__) -# Ensure the path uses the directory structure from your setup script -app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////opt/transcription-app/instance/transcriptions.db' -app.config['UPLOAD_FOLDER'] = '/opt/transcription-app/uploads' # Use absolute path based on setup -app.config['MAX_CONTENT_LENGTH'] = 250 * 1024 * 1024 # 250MB max file size -db = SQLAlchemy() -db.init_app(app) - -# Ensure upload and instance directories exist -os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) -# Assuming the instance folder is handled correctly by Flask or created by setup.sh -# os.makedirs(os.path.dirname(app.config['SQLALCHEMY_DATABASE_URI'].replace('sqlite:///', '/')), exist_ok=True) - - -# --- Database Models --- -class Recording(db.Model): - id = db.Column(db.Integer, primary_key=True) - # Title will now often be AI-generated, maybe start with filename? - title = db.Column(db.String(200), nullable=True) # Allow Null initially - participants = db.Column(db.String(500)) - notes = db.Column(db.Text) - transcription = db.Column(db.Text, nullable=True) - summary = db.Column(db.Text, nullable=True) # <-- ADDED: Summary field - status = db.Column(db.String(50), default='PENDING') # PENDING, PROCESSING, SUMMARIZING, COMPLETED, FAILED - audio_path = db.Column(db.String(500)) - created_at = db.Column(db.DateTime, default=datetime.utcnow) - meeting_date = db.Column(db.Date, nullable=True) # <-- ADDED: Meeting Date field - file_size = db.Column(db.Integer) # Store file size in bytes - - def to_dict(self): - return { - 'id': self.id, - 'title': self.title, - 'participants': self.participants, - 'notes': self.notes, - 'notes_html': md_to_html(self.notes) if self.notes else "", - 'transcription': self.transcription, - 'summary': self.summary, - 'summary_html': md_to_html(self.summary) if self.summary else "", - 'status': self.status, - 'created_at': self.created_at.isoformat() if self.created_at else None, - 'meeting_date': self.meeting_date.isoformat() if self.meeting_date else None, # <-- ADDED: Include meeting_date - 'file_size': self.file_size - } - -with app.app_context(): - db.create_all() - -# --- API client setup for OpenRouter --- -# Use environment variables from .env -openrouter_api_key = os.environ.get("OPENROUTER_API_KEY") -openrouter_base_url = os.environ.get("OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1") -openrouter_model_name = os.environ.get("OPENROUTER_MODEL_NAME", "openai/gpt-3.5-turbo") # Default if not set - -http_client_no_proxy = httpx.Client(verify=True) # verify=True is default, but good to be explicit - -if not openrouter_api_key: - app.logger.warning("OPENROUTER_API_KEY not found. Title/Summary generation DISABLED.") -else: - try: - # ---> Pass the custom httpx_client <--- - client = OpenAI( - api_key=openrouter_api_key, - base_url=openrouter_base_url, - http_client=http_client_no_proxy # Pass the proxy-disabled client - ) - app.logger.info(f"OpenRouter client initialized. Using model: {openrouter_model_name}") - except Exception as client_init_e: - app.logger.error(f"Failed to initialize OpenRouter client: {client_init_e}", exc_info=True) - -# Store details for the transcription client (potentially different) -transcription_api_key = os.environ.get("OPENAI_API_KEY", "cant-be-empty") -transcription_base_url = os.environ.get("OPENAI_BASE_URL", "http://192.168.68.85:1611/v1/") - -app.logger.info(f"Using OpenRouter model for summaries: {openrouter_model_name}") - -# --- Background Transcription & Summarization Task --- -def transcribe_audio_task(app_context, recording_id, filepath, original_filename): - """Runs the transcription and summarization in a background thread.""" - with app_context: # Need app context for db operations in thread - recording = db.session.get(Recording, recording_id) - if not recording: - app.logger.error(f"Error: Recording {recording_id} not found for transcription.") - return - - try: - app.logger.info(f"Starting transcription for recording {recording_id} ({original_filename})...") - recording.status = 'PROCESSING' - db.session.commit() - - # --- Step 1: Transcription --- - with open(filepath, 'rb') as audio_file: - # NOTE: This still uses the hardcoded Whisper model. - # If you want OpenRouter for transcription too, change the model here - # and potentially adjust the API call if needed. - # For now, assuming Whisper via a local compatible endpoint. - # You might need a *separate* client for Whisper if it's at a different URL. - # Example using the configured client (assuming it points to Whisper or OpenRouter handles it): - transcription_client = OpenAI( - api_key=transcription_api_key, - base_url=transcription_base_url, - http_client=http_client_no_proxy # Reuse the same client configuration - ) - transcript = transcription_client.audio.transcriptions.create( - model="Systran/faster-distil-whisper-large-v3", # Your Whisper model - file=audio_file, - language="en" # Specify language if known - ) - recording.transcription = transcript.text - app.logger.info(f"Transcription completed for recording {recording_id}. Text length: {len(recording.transcription)}") - # Don't commit yet, proceed to summarization - - # --- Step 2: Title & Summary Generation using OpenRouter --- - if client is None: # Check if OpenRouter client initialized successfully earlier - app.logger.warning(f"Skipping summary for {recording_id}: OpenRouter client not configured.") - recording.summary = "[Summary skipped: OpenRouter client not configured]" - recording.status = 'COMPLETED' - db.session.commit() - return # Exit cleanly - - recording.status = 'SUMMARIZING' # Update status - db.session.commit() - app.logger.info(f"Requesting title and summary from OpenRouter for recording {recording_id} using model {openrouter_model_name}...") - - if not recording.transcription or len(recording.transcription.strip()) < 10: # Basic check for valid transcript - app.logger.warning(f"Transcription for recording {recording_id} is too short or empty. Skipping summarization.") - recording.status = 'COMPLETED' # Mark as completed even without summary - recording.summary = "[Summary skipped due to short transcription]" - db.session.commit() - return # Exit the task cleanly - - # Prepare the prompt for OpenRouter - prompt_text = f"""Analyze the following audio transcription and generate a concise title and a brief summary. - -Transcription: -\"\"\" -{recording.transcription[:30000]} -\"\"\" - -Respond STRICTLY with a JSON object containing two keys: "title" (a short, descriptive title, max 15 words) and "summary" (a paragraph summarizing the key points, max 150 words). -Example Format: -{{ - "title": "Example Meeting Discussion on Q3 Results", - "summary": "The meeting covered the financial results for Q3, highlighting key achievements and areas for improvement. Action items were assigned for follow-up." -}} - -JSON Response:""" # The prompt guides the model towards the desired output - - try: - # Use the OpenRouter client configured earlier - completion = client.chat.completions.create( - model=openrouter_model_name, - messages=[ - {"role": "system", "content": "You are an AI assistant that generates titles and summaries for meeting transcripts. Respond only with the requested JSON object."}, - {"role": "user", "content": prompt_text} - ], - temperature=0.5, # Adjust temperature as needed - max_tokens=300, # Adjust based on expected title+summary length - response_format={"type": "json_object"} # Request JSON output - ) - - response_content = completion.choices[0].message.content - app.logger.debug(f"Raw OpenRouter response for {recording_id}: {response_content}") - - try: - response_content = completion.choices[0].message.content - app.logger.debug(f"Raw OpenRouter response for {recording_id}: {response_content}") - - # Use regex to extract JSON content from potential markdown code blocks - # This looks for content between markdown code blocks or just takes the whole content - json_match = re.search(r'```(?:json)?(.*?)```|(.+)', response_content, re.DOTALL) - - if json_match: - # Use the first group that matched (either between ``` or the whole content) - sanitized_response = json_match.group(1) if json_match.group(1) else json_match.group(2) - sanitized_response = sanitized_response.strip() - else: - sanitized_response = response_content.strip() - - summary_data = json.loads(response_content) - generated_title = summary_data.get("title") - generated_summary = summary_data.get("summary") - - if generated_title and generated_summary: - # Update recording with AI generated content - recording.title = generated_title.strip() - recording.summary = generated_summary.strip() - recording.status = 'COMPLETED' - app.logger.info(f"Title and summary generated successfully for recording {recording_id}.") - else: - app.logger.warning(f"OpenRouter response for {recording_id} lacked 'title' or 'summary' key. Response: {response_content}") - recording.summary = "[AI summary generation failed: Invalid JSON structure]" - recording.status = 'COMPLETED' # Still completed, but summary failed - - except json.JSONDecodeError as json_e: - app.logger.error(f"Failed to parse JSON response from OpenRouter for {recording_id}: {json_e}. Response: {response_content}") - recording.summary = f"[AI summary generation failed: Invalid JSON response ({json_e})]" - recording.status = 'COMPLETED' # Mark as completed, summary failed - - except Exception as summary_e: - app.logger.error(f"Error calling OpenRouter API for summary ({recording_id}): {str(summary_e)}") - # Keep transcription, but mark summary failed. Don't change status from SUMMARIZING yet. - recording.summary = f"[AI summary generation failed: API Error ({str(summary_e)})]" - recording.status = 'COMPLETED' # Even if summary fails, transcription worked. - - - db.session.commit() # Final commit for this step - - except Exception as e: - db.session.rollback() # Rollback if any step failed critically - app.logger.error(f"Processing FAILED for recording {recording_id}: {str(e)}", exc_info=True) - # Retrieve recording again in case session was rolled back - recording = db.session.get(Recording, recording_id) - if recording: - # Ensure status reflects failure even after rollback/retrieve attempt - if recording.status not in ['COMPLETED', 'FAILED']: # Avoid overwriting final state - recording.status = 'FAILED' - if not recording.transcription: # If transcription itself failed - recording.transcription = f"Processing failed: {str(e)}" - # Add error note to summary if appropriate stage was reached - if recording.status == 'SUMMARIZING' and not recording.summary: - recording.summary = f"[Processing failed during summarization: {str(e)}]" - - db.session.commit() - -# --- Chat with Transcription --- -@app.route('/chat', methods=['POST']) -def chat_with_transcription(): - try: - data = request.json - if not data: - return jsonify({'error': 'No data provided'}), 400 - - recording_id = data.get('recording_id') - user_message = data.get('message') - message_history = data.get('message_history', []) - - if not recording_id: - return jsonify({'error': 'No recording ID provided'}), 400 - if not user_message: - return jsonify({'error': 'No message provided'}), 400 - - # Get the recording - recording = db.session.get(Recording, recording_id) - if not recording: - return jsonify({'error': 'Recording not found'}), 404 - - # Check if OpenRouter client is available - if client is None: - return jsonify({'error': 'Chat service is not available (OpenRouter client not configured)'}), 503 - - # Prepare the system prompt with the transcription - system_prompt = f"""You are a professional meeting analyst working with Murtaza Nasir, Assistant Professor at Wichita State University. Analyze the following meeting information and respond to the specific request. - -Following are the meeting participants and their roles: -{recording.participants or "No specific participants information provided."} - -Following is the meeting transcript: -<> -{recording.transcription or "No transcript available."} -<> - -Additional context and notes about the meeting: -{recording.notes or "none"} -""" - - # Call the LLM - try: - # Prepare messages array with system prompt and conversation history - messages = [{"role": "system", "content": system_prompt}] - - # Add message history if provided - if message_history: - messages.extend(message_history) - - # Add the current user message - messages.append({"role": "user", "content": user_message}) - - completion = client.chat.completions.create( - model=openrouter_model_name, - messages=messages, - temperature=0.7, - max_tokens=1000 - ) - - response_content = completion.choices[0].message.content - - # Convert markdown in the response to HTML - response_html = md_to_html(response_content) - - return jsonify({ - 'response': response_content, - 'response_html': response_html, - 'success': True - }) - - except Exception as chat_error: - app.logger.error(f"Error calling OpenRouter API for chat: {str(chat_error)}") - return jsonify({'error': f'Chat service error: {str(chat_error)}'}), 500 - - except Exception as e: - app.logger.error(f"Error in chat endpoint: {str(e)}") - return jsonify({'error': str(e)}), 500 - - -# --- Flask Routes --- -@app.route('/') -def index(): - return render_template('index.html') - -@app.route('/recordings', methods=['GET']) -def get_recordings(): - try: - stmt = select(Recording).order_by(Recording.created_at.desc()) - recordings = db.session.execute(stmt).scalars().all() - return jsonify([recording.to_dict() for recording in recordings]) - except Exception as e: - app.logger.error(f"Error fetching recordings: {e}") - return jsonify({'error': str(e)}), 500 - -@app.route('/save', methods=['POST']) -def save_metadata(): - try: - data = request.json - if not data: return jsonify({'error': 'No data provided'}), 400 - recording_id = data.get('id') - if not recording_id: return jsonify({'error': 'No recording ID provided'}), 400 - - recording = db.session.get(Recording, recording_id) - if not recording: return jsonify({'error': 'Recording not found'}), 404 - - # Update fields if provided - if 'title' in data: recording.title = data['title'] - if 'participants' in data: recording.participants = data['participants'] - if 'notes' in data: recording.notes = data['notes'] - if 'summary' in data: recording.summary = data['summary'] # <-- ADDED: Allow saving edited summary - if 'meeting_date' in data: - try: - # Attempt to parse date string (e.g., "YYYY-MM-DD") - date_str = data['meeting_date'] - if date_str: - recording.meeting_date = datetime.strptime(date_str, '%Y-%m-%d').date() - else: - recording.meeting_date = None # Allow clearing the date - except (ValueError, TypeError) as e: - app.logger.warning(f"Could not parse meeting_date '{data.get('meeting_date')}': {e}") - # Optionally return an error or just ignore the invalid date - # return jsonify({'error': f"Invalid date format for meeting_date. Use YYYY-MM-DD."}), 400 - - # Do not update transcription or status here - db.session.commit() - return jsonify({'success': True, 'recording': recording.to_dict()}) - - except Exception as e: - db.session.rollback() - app.logger.error(f"Error saving metadata for recording {recording_id}: {e}") - return jsonify({'error': str(e)}), 500 - - -@app.route('/upload', methods=['POST']) -def upload_file(): - try: - if 'file' not in request.files: - return jsonify({'error': 'No file provided'}), 400 - - file = request.files['file'] - if file.filename == '': - return jsonify({'error': 'No file selected'}), 400 - - filename = secure_filename(file.filename) - # Ensure filepath uses the configured UPLOAD_FOLDER - filepath = os.path.join(app.config['UPLOAD_FOLDER'], f"{datetime.now().strftime('%Y%m%d%H%M%S')}_{filename}") - - # Get file size before saving - file.seek(0, os.SEEK_END) - file_size = file.tell() - file.seek(0) - - # Check size limit again - if file_size > app.config['MAX_CONTENT_LENGTH']: - raise RequestEntityTooLarge() - - file.save(filepath) - app.logger.info(f"File saved to {filepath}") - - # Create initial database entry with PENDING status and filename as placeholder title - recording = Recording( - audio_path=filepath, - # Use filename (without path part) as initial title - title=f"Recording - {filename}", - file_size=file_size, - status='PENDING', # Explicitly set status - meeting_date=datetime.utcnow().date() # <-- ADDED: Default meeting_date to today - ) - db.session.add(recording) - db.session.commit() - app.logger.info(f"Initial recording record created with ID: {recording.id}") - - # --- Start transcription & summarization in background thread --- - thread = threading.Thread( - target=transcribe_audio_task, - # Pass original filename for logging clarity - args=(app.app_context(), recording.id, filepath, filename) - ) - thread.start() - app.logger.info(f"Background processing thread started for recording ID: {recording.id}") - - # Return the initial recording data and ID immediately - return jsonify(recording.to_dict()), 202 # 202 Accepted - - except RequestEntityTooLarge: - max_size_mb = app.config['MAX_CONTENT_LENGTH'] / (1024 * 1024) - app.logger.warning(f"Upload failed: File too large (>{max_size_mb}MB)") - return jsonify({ - 'error': f'File too large. Maximum size is {max_size_mb:.0f} MB.', - 'max_size_mb': max_size_mb - }), 413 - except Exception as e: - db.session.rollback() # Rollback if initial save failed - app.logger.error(f"Error during file upload: {e}", exc_info=True) - return jsonify({'error': str(e)}), 500 - - -# Status Endpoint (no changes needed, it returns the full recording dict) -@app.route('/status/', methods=['GET']) -def get_status(recording_id): - """Endpoint to check the transcription/summarization status.""" - try: - recording = db.session.get(Recording, recording_id) - if not recording: - return jsonify({'error': 'Recording not found'}), 404 - return jsonify(recording.to_dict()) - except Exception as e: - app.logger.error(f"Error fetching status for recording {recording_id}: {e}") - return jsonify({'error': str(e)}), 500 - -# Get Audio Endpoint (no changes needed) -@app.route('/audio/') -def get_audio(recording_id): - try: - recording = db.session.get(Recording, recording_id) - if not recording or not recording.audio_path: - return jsonify({'error': 'Recording or audio file not found'}), 404 - if not os.path.exists(recording.audio_path): - app.logger.error(f"Audio file missing from server: {recording.audio_path}") - return jsonify({'error': 'Audio file missing from server'}), 404 - return send_file(recording.audio_path) - except Exception as e: - app.logger.error(f"Error serving audio for recording {recording_id}: {e}") - return jsonify({'error': str(e)}), 500 - -# Delete Recording Endpoint (no changes needed functionally) -@app.route('/recording/', methods=['DELETE']) -def delete_recording(recording_id): - try: - recording = db.session.get(Recording, recording_id) - if not recording: - return jsonify({'error': 'Recording not found'}), 404 - - # Delete the audio file first - try: - if recording.audio_path and os.path.exists(recording.audio_path): - os.remove(recording.audio_path) - app.logger.info(f"Deleted audio file: {recording.audio_path}") - except Exception as e: - app.logger.error(f"Error deleting audio file {recording.audio_path}: {e}") - - # Delete the database record - db.session.delete(recording) - db.session.commit() - app.logger.info(f"Deleted recording record ID: {recording_id}") - - return jsonify({'success': True}) - except Exception as e: - db.session.rollback() - app.logger.error(f"Error deleting recording {recording_id}: {e}") - return jsonify({'error': str(e)}), 500 - - -if __name__ == '__main__': - # Consider using waitress or gunicorn for production - # waitress-serve --host 0.0.0.0 --port 8899 app:app - # For development: - app.run(host='0.0.0.0', port=8899, debug=True) # Set debug=False if thread issues arise diff --git a/config/docker-compose.example.yml b/config/docker-compose.example.yml new file mode 100644 index 00000000..3e238dd4 --- /dev/null +++ b/config/docker-compose.example.yml @@ -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: learnedmachine/speakr:latest + container_name: speakr + 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 Speakr: + # 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: + # - speakr-uploads:/data/uploads + # - speakr-instance:/data/instance + # # Optional: Uncomment if using auto-export feature + # # - speakr-exports:/data/exports + # # Optional: Uncomment if using auto-processing feature + # # - speakr-auto-process:/data/auto-process diff --git a/config/env.asr.example b/config/env.asr.example new file mode 100644 index 00000000..158db342 --- /dev/null +++ b/config/env.asr.example @@ -0,0 +1,269 @@ +# ----------------------------------------------------------------------------- +# Speakr 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 Timeout & Retry --- +# Read timeout for LLM requests in seconds (default 600). Increase for local +# models (Ollama, vLLM) that need more time for long transcripts. +# Connect/write timeouts stay at 30s so bad URLs fail fast. +# LLM_REQUEST_TIMEOUT=1800 +# +# Max retries on timeout (default 2). Set to 0 for local inference to avoid +# queuing duplicate requests when your model is still processing the first one. +# LLM_MAX_RETRIES=0 + +# --- 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 diff --git a/config/env.email.example b/config/env.email.example new file mode 100644 index 00000000..80590f7d --- /dev/null +++ b/config/env.email.example @@ -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 diff --git a/config/env.sso.example b/config/env.sso.example new file mode 100644 index 00000000..418c9d64 --- /dev/null +++ b/config/env.sso.example @@ -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 + diff --git a/config/env.transcription.example b/config/env.transcription.example new file mode 100644 index 00000000..5d91e020 --- /dev/null +++ b/config/env.transcription.example @@ -0,0 +1,335 @@ +# ============================================================================= +# Transcription Connector Configuration +# ============================================================================= +# +# Speakr 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) +# ============================================================================= +# Speakr 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, mistral, vibevoice +# 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 + +# ============================================================================= +# MISTRAL / VOXTRAL CONFIGURATION +# ============================================================================= +# Mistral's Voxtral model provides cloud-based transcription with built-in +# speaker diarization and language detection. Requires a Mistral API key. +# +# TRANSCRIPTION_CONNECTOR=mistral +# TRANSCRIPTION_API_KEY=your_mistral_api_key +# TRANSCRIPTION_MODEL=voxtral-mini-latest +# +# To use a custom Mistral-compatible endpoint: +# TRANSCRIPTION_BASE_URL=https://api.mistral.ai + +# ============================================================================= +# VIBEVOICE CONFIGURATION (Self-hosted via vLLM) +# ============================================================================= +# Microsoft's VibeVoice ASR model provides transcription with speaker +# diarization, timestamps, and language detection for 50+ languages. It runs +# on your own hardware via vLLM and handles up to 60 minutes per request. +# Longer files are automatically chunked by the app. +# +# Requirements: +# - vLLM server with the VibeVoice model loaded +# - GPU(s) with enough VRAM (bf16 needs ~18GB, fits on 2x consumer GPUs) +# +# TRANSCRIPTION_CONNECTOR=vibevoice +# TRANSCRIPTION_BASE_URL=http://your-vllm-server:8000 +# TRANSCRIPTION_MODEL=vibevoice +# +# If your vLLM server requires authentication: +# TRANSCRIPTION_API_KEY=your_api_key + +# ============================================================================= +# CHUNKING CONFIGURATION (For large files) +# ============================================================================= +# Chunking is now connector-aware with this priority: +# 1. Connector handles internally (openai_transcribe, asr_endpoint, mistral) → 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/mistral: These settings are IGNORED (connector handles it) +# For openai_whisper: These settings control chunking behavior +# For vibevoice: App chunks files >58 min into ~50 min pieces automatically + +# 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 +# +# --- Mistral Voxtral (cloud diarization) --- +# TRANSCRIPTION_CONNECTOR=mistral +# TRANSCRIPTION_API_KEY=your-mistral-key +# TRANSCRIPTION_MODEL=voxtral-mini-latest +# +# --- VibeVoice ASR (self-hosted diarization) --- +# TRANSCRIPTION_CONNECTOR=vibevoice +# TRANSCRIPTION_BASE_URL=http://your-vllm-server:8000 +# TRANSCRIPTION_MODEL=vibevoice + +# ============================================================================= +# 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 + +# ============================================================================= +# ADDITIONAL PROVIDER NOTES +# ============================================================================= +# The connector architecture is designed to support additional providers. +# Currently available: openai_whisper, openai_transcribe, asr_endpoint, +# azure_openai_transcribe, mistral, vibevoice +# +# 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. diff --git a/config/env.whisper.example b/config/env.whisper.example new file mode 100644 index 00000000..4971fdf5 --- /dev/null +++ b/config/env.whisper.example @@ -0,0 +1,266 @@ +# ----------------------------------------------------------------------------- +# Speakr 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 Timeout & Retry --- +# Read timeout for LLM requests in seconds (default 600). Increase for local +# models (Ollama, vLLM) that need more time for long transcripts. +# Connect/write timeouts stay at 30s so bad URLs fail fast. +# LLM_REQUEST_TIMEOUT=1800 +# +# Max retries on timeout (default 2). Set to 0 for local inference to avoid +# queuing duplicate requests when your model is still processing the first one. +# LLM_MAX_RETRIES=0 + +# --- 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 diff --git a/config/env.whisperx.example b/config/env.whisperx.example new file mode 100644 index 00000000..2198be8b --- /dev/null +++ b/config/env.whisperx.example @@ -0,0 +1,241 @@ +# ----------------------------------------------------------------------------- +# Speakr 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 diff --git a/constraints.txt b/constraints.txt new file mode 100644 index 00000000..51369e32 --- /dev/null +++ b/constraints.txt @@ -0,0 +1 @@ +scipy<1.15 diff --git a/deployment/setup.sh b/deployment/setup.sh index 9a7c70fa..5a469f4f 100755 --- a/deployment/setup.sh +++ b/deployment/setup.sh @@ -1,6 +1,8 @@ #!/bin/bash # Create directory for the application +sudo systemctl stop transcription + sudo mkdir -p /opt/transcription-app sudo chown $USER:$USER /opt/transcription-app @@ -8,9 +10,8 @@ sudo chown $USER:$USER /opt/transcription-app cp app.py /opt/transcription-app/ cp -r templates /opt/transcription-app/ cp requirements.txt /opt/transcription-app/ -cp reset_db.py /opt/transcription-app/ -cp migrate_db.py /opt/transcription-app/ -cp create_admin.py /opt/transcription-app/ +cp scripts/reset_db.py /opt/transcription-app/ +cp scripts/create_admin.py /opt/transcription-app/ cp .env /opt/transcription-app/ # Copy the .env file with API keys # Add SECRET_KEY to .env file if it doesn't exist @@ -70,6 +71,7 @@ EOF # Reload systemd and start service sudo systemctl daemon-reload sudo systemctl restart transcription +sudo systemctl disable transcription sudo systemctl enable transcription # Check service status diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..9c4f4ad5 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,31 @@ +# Jekyll build output +_site/ +.sass-cache/ +.jekyll-cache/ +.jekyll-metadata + +# Bundle directory +.bundle/ +vendor/ + +# Local development files +_config_local.yml +local-serve.sh +docker-serve.sh +simple-serve.sh +serve-local.sh + +# OS files +.DS_Store +Thumbs.db + +# IDE files +.vscode/ +.idea/ + +# Ruby version files +.ruby-version +.ruby-gemset + +# Documentation verification (internal use) +DOCUMENTATION_VERIFICATION_CHECKLIST.md \ No newline at end of file diff --git a/docs/Dockerfile b/docs/Dockerfile new file mode 100644 index 00000000..99ce2b2a --- /dev/null +++ b/docs/Dockerfile @@ -0,0 +1,28 @@ +FROM python:3.11-slim + +WORKDIR /app + +# Install git (required for git-revision-date plugin) +RUN apt-get update && \ + apt-get install -y git && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# Install requirements +COPY requirements-docs.txt . +RUN pip install --no-cache-dir -r requirements-docs.txt + +# Create docs directory structure +RUN mkdir -p docs + +# Copy mkdocs config to root +COPY mkdocs.yml . + +# Copy all documentation files to docs subdirectory +COPY . ./docs/ + +# Expose MkDocs development server port +EXPOSE 8000 + +# Run MkDocs server +CMD ["mkdocs", "serve", "--dev-addr=0.0.0.0:8000"] \ No newline at end of file diff --git a/docs/Gemfile.simple b/docs/Gemfile.simple new file mode 100644 index 00000000..9ca69719 --- /dev/null +++ b/docs/Gemfile.simple @@ -0,0 +1,8 @@ +source "https://rubygems.org" + +gem "jekyll", "~> 4.3" +gem "jekyll-seo-tag" +gem "jekyll-sitemap" +gem "jekyll-feed" +gem "webrick", "~> 1.8" +gem "kramdown-parser-gfm" \ No newline at end of file diff --git a/docs/PUSH_NOTIFICATIONS_SETUP.md b/docs/PUSH_NOTIFICATIONS_SETUP.md new file mode 100644 index 00000000..df4c824d --- /dev/null +++ b/docs/PUSH_NOTIFICATIONS_SETUP.md @@ -0,0 +1,324 @@ +# Push Notifications Setup Guide + +This guide explains how to complete the push notification setup for Speakr. + +## Overview + +The client-side push notification infrastructure is now complete. To enable push notifications, you need to: + +1. Generate VAPID keys +2. Configure the client with the public key +3. Implement backend endpoints to store subscriptions and send notifications + +## Step 1: Generate VAPID Keys + +### Method A: Using web-push (Node.js) + +```bash +npm install -g web-push +web-push generate-vapid-keys +``` + +### Method B: Using Python + +```bash +pip install pywebpush +``` + +```python +from pywebpush import vapid_keys + +vapid_keys = vapid_keys() +print("Public Key:", vapid_keys['publicKey']) +print("Private Key:", vapid_keys['privateKey']) +``` + +### Method C: Using pywebpush CLI + +```bash +pywebpush generate-vapid-keys +``` + +**IMPORTANT:** Keep the private key secret! Never commit it to version control. + +## Step 2: Configure Client + +1. Open `static/js/config/push-config.js` +2. Set `ENABLED: true` +3. Add your VAPID public key to `VAPID_PUBLIC_KEY` +4. Update `CONTACT_INFO` with your admin email or website + +```javascript +export const PUSH_CONFIG = { + ENABLED: true, + VAPID_PUBLIC_KEY: 'YOUR_PUBLIC_KEY_HERE', + CONTACT_INFO: 'mailto:admin@yourdomain.com' +}; +``` + +## Step 3: Implement Backend Endpoints + +### Required Backend Endpoints + +#### 1. Store Push Subscription + +**Endpoint:** `POST /api/push/subscribe` + +**Purpose:** Save user's push subscription to database + +**Request Body:** +```json +{ + "endpoint": "https://fcm.googleapis.com/fcm/send/...", + "keys": { + "p256dh": "...", + "auth": "..." + } +} +``` + +**Response:** +```json +{ + "success": true, + "message": "Subscription saved" +} +``` + +**Implementation Example (Flask):** + +```python +from flask import Blueprint, request, jsonify +from flask_login import login_required, current_user +from models import db, PushSubscription + +push_bp = Blueprint('push', __name__) + +@push_bp.route('/api/push/subscribe', methods=['POST']) +@login_required +def subscribe(): + """Store push subscription for current user""" + subscription_data = request.json + + # Check if subscription already exists + existing = PushSubscription.query.filter_by( + user_id=current_user.id, + endpoint=subscription_data['endpoint'] + ).first() + + if existing: + return jsonify({'success': True, 'message': 'Already subscribed'}) + + # Create new subscription + subscription = PushSubscription( + user_id=current_user.id, + endpoint=subscription_data['endpoint'], + p256dh_key=subscription_data['keys']['p256dh'], + auth_key=subscription_data['keys']['auth'] + ) + + db.session.add(subscription) + db.session.commit() + + return jsonify({'success': True, 'message': 'Subscription saved'}) +``` + +#### 2. Remove Push Subscription + +**Endpoint:** `POST /api/push/unsubscribe` + +**Purpose:** Remove user's push subscription from database + +**Request Body:** Same as subscribe + +**Response:** +```json +{ + "success": true, + "message": "Subscription removed" +} +``` + +**Implementation Example:** + +```python +@push_bp.route('/api/push/unsubscribe', methods=['POST']) +@login_required +def unsubscribe(): + """Remove push subscription for current user""" + subscription_data = request.json + + subscription = PushSubscription.query.filter_by( + user_id=current_user.id, + endpoint=subscription_data['endpoint'] + ).first() + + if subscription: + db.session.delete(subscription) + db.session.commit() + return jsonify({'success': True, 'message': 'Subscription removed'}) + + return jsonify({'success': False, 'message': 'Subscription not found'}), 404 +``` + +## Step 4: Database Model + +Add a `PushSubscription` model to your database: + +```python +from models import db +from sqlalchemy import Column, Integer, String, ForeignKey, DateTime +from sqlalchemy.sql import func + +class PushSubscription(db.Model): + __tablename__ = 'push_subscriptions' + + id = Column(Integer, primary_key=True) + user_id = Column(Integer, ForeignKey('users.id'), nullable=False) + endpoint = Column(String(500), nullable=False, unique=True) + p256dh_key = Column(String(200), nullable=False) + auth_key = Column(String(100), nullable=False) + created_at = Column(DateTime, server_default=func.now()) + + __table_args__ = ( + db.Index('idx_user_endpoint', 'user_id', 'endpoint'), + ) +``` + +Create the migration: + +```bash +flask db migrate -m "Add push subscriptions table" +flask db upgrade +``` + +## Step 5: Send Push Notifications + +Use the `pywebpush` library to send notifications when transcription is complete: + +```python +from pywebpush import webpush, WebPushException +import json +import os + +def send_push_notification(user_id, title, body, data=None): + """Send push notification to all subscriptions for a user""" + subscriptions = PushSubscription.query.filter_by(user_id=user_id).all() + + vapid_private_key = os.getenv('VAPID_PRIVATE_KEY') + vapid_contact = os.getenv('VAPID_CONTACT', 'mailto:admin@example.com') + + notification_data = { + 'title': title, + 'body': body, + 'icon': '/static/img/icon-192x192.png', + 'badge': '/static/img/icon-192x192.png', + 'data': data or {} + } + + for subscription in subscriptions: + try: + webpush( + subscription_info={ + 'endpoint': subscription.endpoint, + 'keys': { + 'p256dh': subscription.p256dh_key, + 'auth': subscription.auth_key + } + }, + data=json.dumps(notification_data), + vapid_private_key=vapid_private_key, + vapid_claims={'sub': vapid_contact} + ) + print(f'Push notification sent to user {user_id}') + except WebPushException as e: + print(f'Failed to send push to {subscription.endpoint}: {e}') + # If subscription is expired, remove it + if e.response and e.response.status_code in [404, 410]: + db.session.delete(subscription) + db.session.commit() +``` + +## Step 6: Integrate with Transcription + +Call the push notification function when transcription is complete: + +```python +# In your transcription completion handler +def on_transcription_complete(recording_id): + recording = AudioFile.query.get(recording_id) + + if recording: + send_push_notification( + user_id=recording.user_id, + title='Transcription Complete', + body=f'"{recording.display_name or recording.filename}" has been transcribed', + data={ + 'recording_id': recording_id, + 'url': f'/recording/{recording_id}' + } + ) +``` + +## Step 7: Environment Variables + +Add these environment variables to your `.env` file: + +```bash +# VAPID keys for push notifications +VAPID_PRIVATE_KEY=your_private_key_here +VAPID_CONTACT=mailto:admin@yourdomain.com +``` + +## Testing Push Notifications + +1. Open the app in a browser +2. Open Developer Tools > Console +3. Run: `await pwaComposable.subscribeToPushNotifications()` +4. Check database to verify subscription was saved +5. Trigger a test notification from the backend +6. Verify notification appears + +## Browser Support + +| Browser | Desktop | Mobile | +|---------|---------|--------| +| Chrome | ✅ | ✅ | +| Edge | ✅ | ✅ | +| Firefox | ✅ | ✅ | +| Safari | ✅ | ⚠️ iOS 16.4+ | +| Opera | ✅ | ✅ | + +**Note:** iOS Safari requires iOS 16.4+ and the app must be added to the home screen. + +## Troubleshooting + +### Subscription fails with "NotAllowedError" +- User denied notification permission +- Ask user to enable notifications in browser settings + +### Subscription not saving on server +- Check backend endpoint is accessible +- Verify CSRF token is valid +- Check server logs for errors + +### Push notifications not received +- Verify VAPID keys match between client and server +- Check subscription is in database +- Test with browser developer tools +- Ensure service worker is registered + +## Security Considerations + +1. **Never expose private VAPID key** - Keep it on server only +2. **Validate subscriptions** - Ensure they belong to authenticated users +3. **Rate limit subscriptions** - Prevent abuse +4. **Clean up expired subscriptions** - Remove 404/410 responses +5. **Use HTTPS** - Required for push notifications + +## Additional Resources + +- [Web Push Protocol](https://datatracker.ietf.org/doc/html/rfc8030) +- [VAPID Specification](https://datatracker.ietf.org/doc/html/rfc8292) +- [pywebpush Documentation](https://github.com/web-push-libs/pywebpush) +- [MDN Push API](https://developer.mozilla.org/en-US/docs/Web/API/Push_API) diff --git a/docs/_includes/sidebar.html b/docs/_includes/sidebar.html new file mode 100644 index 00000000..9e522354 --- /dev/null +++ b/docs/_includes/sidebar.html @@ -0,0 +1,33 @@ + \ No newline at end of file diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html new file mode 100644 index 00000000..9c0c02d6 --- /dev/null +++ b/docs/_layouts/default.html @@ -0,0 +1,403 @@ + + + + + + + + {% seo %} + + + + + + + + + + + +
+ {% include sidebar.html %} + +
+
+ {{ content }} +
+
+
+ + + + \ No newline at end of file diff --git a/docs/_layouts/docs.html b/docs/_layouts/docs.html new file mode 100644 index 00000000..820db4f3 --- /dev/null +++ b/docs/_layouts/docs.html @@ -0,0 +1,22 @@ + + + + + + {% if page.title %}{{ page.title }} - {% endif %}{{ site.title }} + + + + + +
+ {% include sidebar.html %} + +
+
+ {{ content }} +
+
+
+ + \ No newline at end of file diff --git a/docs/admin-guide/email-setup.md b/docs/admin-guide/email-setup.md new file mode 100644 index 00000000..84871b21 --- /dev/null +++ b/docs/admin-guide/email-setup.md @@ -0,0 +1,226 @@ +# Email Verification & Password Reset + +This guide explains how to configure email functionality in Speakr, enabling email verification for new user registrations and password reset capabilities for all users. + +## Overview + +Email features in Speakr are completely opt-in. When configured, they provide: + +- **Email Verification**: Require new users to verify their email address before accessing the system +- **Password Reset**: Allow users to reset forgotten passwords via email + +Both features work independently of domain restrictions—you can use email verification even with open registration (`ALLOW_REGISTRATION=true`) and no domain restrictions. + +## Prerequisites + +- SMTP server credentials (Gmail, SendGrid, Mailgun, Amazon SES, or any SMTP provider) +- Speakr instance accessible via the URL you configure (for email links to work) + +## Configuration + +### Required Environment Variables + +Set these variables in your `.env` file (see `config/env.email.example` for a complete template): + +```bash +# Enable email features +ENABLE_EMAIL_VERIFICATION=true +REQUIRE_EMAIL_VERIFICATION=false + +# SMTP Configuration +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_USERNAME=your-email@gmail.com +SMTP_PASSWORD=your-app-password +SMTP_USE_TLS=true +SMTP_FROM_ADDRESS=noreply@yourdomain.com +SMTP_FROM_NAME=Speakr +``` + +Restart Speakr after updating environment variables. + +### Configuration Options + +| Variable | Default | Description | +|----------|---------|-------------| +| `ENABLE_EMAIL_VERIFICATION` | `false` | Enable email verification for new registrations | +| `REQUIRE_EMAIL_VERIFICATION` | `false` | Block login for unverified users (only works when verification is enabled) | +| `SMTP_HOST` | (none) | SMTP server hostname | +| `SMTP_PORT` | `587` | SMTP server port | +| `SMTP_USERNAME` | (none) | SMTP authentication username | +| `SMTP_PASSWORD` | (none) | SMTP authentication password | +| `SMTP_USE_TLS` | `true` | Use STARTTLS encryption (port 587) | +| `SMTP_USE_SSL` | `false` | Use SSL encryption (port 465) | +| `SMTP_FROM_ADDRESS` | `noreply@yourdomain.com` | Email address shown in "From" field | +| `SMTP_FROM_NAME` | `Speakr` | Display name shown alongside from address | + +### Understanding the Two Verification Modes + +**Soft Verification** (`ENABLE_EMAIL_VERIFICATION=true`, `REQUIRE_EMAIL_VERIFICATION=false`): + +- New users receive a verification email after registration +- Users can log in immediately without verifying +- Useful for encouraging email verification without blocking access + +**Strict Verification** (`ENABLE_EMAIL_VERIFICATION=true`, `REQUIRE_EMAIL_VERIFICATION=true`): + +- New users receive a verification email after registration +- Users cannot log in until they verify their email +- Best for environments requiring confirmed email addresses + +### Combining with Other Registration Settings + +Email verification works seamlessly with other registration controls: + +```bash +# Open registration with email verification +ALLOW_REGISTRATION=true +ENABLE_EMAIL_VERIFICATION=true +REQUIRE_EMAIL_VERIFICATION=true + +# Domain-restricted registration with verification +ALLOW_REGISTRATION=true +REGISTRATION_ALLOWED_DOMAINS=company.com,subsidiary.org +ENABLE_EMAIL_VERIFICATION=true +REQUIRE_EMAIL_VERIFICATION=true + +# Closed registration (admin creates accounts) +ALLOW_REGISTRATION=false +# Email verification not applicable - admin creates verified accounts +``` + +## Provider-Specific Setup + +### Gmail + +```bash +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_USE_TLS=true +SMTP_USERNAME=your-email@gmail.com +SMTP_PASSWORD=your-app-password +``` + +**Important:** Use an [App Password](https://support.google.com/accounts/answer/185833), not your regular Gmail password. App Passwords are required when 2-factor authentication is enabled (recommended). + +### SendGrid + +```bash +SMTP_HOST=smtp.sendgrid.net +SMTP_PORT=587 +SMTP_USE_TLS=true +SMTP_USERNAME=apikey +SMTP_PASSWORD=your-sendgrid-api-key +``` + +### Mailgun + +```bash +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 + +```bash +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 + +```bash +SMTP_HOST=smtp.office365.com +SMTP_PORT=587 +SMTP_USE_TLS=true +SMTP_USERNAME=your-email@yourdomain.com +SMTP_PASSWORD=your-password +``` + +### SSL vs TLS + +- **Port 587 with TLS** (recommended): Set `SMTP_USE_TLS=true`, `SMTP_USE_SSL=false` +- **Port 465 with SSL**: Set `SMTP_USE_TLS=false`, `SMTP_USE_SSL=true` +- **Port 25 (unencrypted)**: Not recommended for security reasons + +## User Experience + +### Registration Flow (with verification enabled) + +1. User fills out registration form +2. Account is created with `email_verified=false` +3. Verification email is sent automatically +4. User sees "Check your email" page with option to resend +5. User clicks verification link in email +6. Account is marked as verified +7. User can now log in (if `REQUIRE_EMAIL_VERIFICATION=true`) + +### Password Reset Flow + +1. User clicks "Forgot password?" on login page +2. User enters their email address +3. If account exists, reset email is sent (no indication if account doesn't exist for security) +4. User clicks reset link in email +5. User sets new password +6. User is redirected to login + +### Token Expiry + +- **Email verification links**: Valid for 24 hours +- **Password reset links**: Valid for 1 hour + +Users can request new links if their tokens expire. + +## Migration Behavior + +When enabling email verification on an existing instance: + +- **Existing users are automatically marked as verified** (grandfathered) +- Only new registrations after enabling the feature require verification +- No action needed for current users + +## Security Considerations + +1. **Use secure SMTP connections**: Always enable TLS or SSL +2. **Use app-specific passwords**: When available (Gmail, etc.) +3. **Set a strong SECRET_KEY**: Token security depends on your Flask secret key +4. **Consider dedicated email services**: SendGrid, Mailgun, and SES offer better deliverability than personal email accounts + +## Troubleshooting + +### Emails not sending + +1. Check Docker logs: `docker compose logs -f app` +2. Verify SMTP credentials are correct +3. Ensure SMTP port is not blocked by firewall +4. Try sending a test email using the same credentials from another tool + +### Emails going to spam + +1. Use a proper `SMTP_FROM_ADDRESS` that matches your domain +2. Configure SPF and DKIM records for your domain +3. Consider using a dedicated email service with good reputation + +### Verification link not working + +1. Ensure `SECRET_KEY` hasn't changed since the email was sent +2. Check if the link has expired (24 hours for verification, 1 hour for reset) +3. Verify your Speakr instance is accessible at the URL in the email + +### "SMTP not configured" errors + +Ensure all required SMTP variables are set: + +- `SMTP_HOST` +- `SMTP_USERNAME` +- `SMTP_PASSWORD` + +--- + +Next: [SSO Setup](sso-setup.md) → diff --git a/docs/admin-guide/group-management.md b/docs/admin-guide/group-management.md new file mode 100644 index 00000000..65afe227 --- /dev/null +++ b/docs/admin-guide/group-management.md @@ -0,0 +1,414 @@ +--- +layout: default +title: Group Management +parent: Admin Guide +nav_order: 6 +--- + +# Group Management + +Groups enable organized collaboration in multi-user Speakr instances by grouping users and automating recording access through group-specific tags. This powerful feature reduces administrative overhead while maintaining security and control over content access. + +## Prerequisites + +Before enabling groups, ensure internal sharing is configured. Groups build on Speakr's internal sharing infrastructure to automatically grant access when users apply group tags. + +### Required Configuration + +Add these settings to your `.env` file: + +```bash +# Enable internal sharing (required for groups) +ENABLE_INTERNAL_SHARING=true + +# Control username visibility +SHOW_USERNAMES_IN_UI=true # Show usernames in UI +# OR +SHOW_USERNAMES_IN_UI=false # Hide usernames (users type usernames manually to share) +``` + +After modifying `.env`, restart your Speakr instance for changes to take effect. The Groups tab will appear in the admin dashboard once internal sharing is enabled. + +### Privacy Considerations + +The `SHOW_USERNAMES_IN_UI` setting affects the entire instance. When enabled (`true`), users see actual usernames when searching for colleagues and viewing shared content. This improves usability in small, trusted groups where everyone knows each other. + +When disabled (`false`), usernames are hidden from the interface. Users must know each other's usernames to share recordings - they type the username manually when creating shares. This privacy-focused approach suits organizations where username visibility should be restricted. Group functionality works identically in both modes - only the display changes. + +## Creating and Managing Groups + +Groups are created and managed exclusively through the admin dashboard. Regular users cannot create groups, ensuring centralized control over organizational structure. + +### Creating a Group + +Navigate to the Admin Dashboard and select the Groups tab. Click "Create Group" to open the creation modal. Provide a group name that clearly identifies the group's purpose - "Engineering", "Sales EMEA", "Project Phoenix", etc. Descriptive names help users understand each group's scope and purpose. + +The description field is optional but recommended. Use it to explain the group's purpose, which projects or departments it serves, or who should be members. Good descriptions help future administrators understand group organization and make membership decisions. + +Click "Create Group" to finalize creation. The group appears in your groups list immediately, though it starts with no members. The creating user (you) doesn't automatically join - membership must be explicitly granted even for group creators. + +### Managing Group Membership + +Click the users-cog icon next to any group to open the group management modal. This interface shows current members, their roles, and provides tools for adding or removing members. + +To add a member, select a user from the dropdown and choose their role: + +- **Member**: Can use group tags and access group-tagged recordings. Suitable for most group participants who need to collaborate on content. +- **Admin**: All member capabilities plus the ability to manage group membership, create and delete group tags, and access group management features. Useful for group leads or managers who need administrative control. + +Click "Add Member" to grant access. The user immediately gains visibility to group tags and will receive future group-tagged recordings. They don't automatically gain access to existing group-tagged recordings - only new ones tagged after they join. + +### Changing Member Roles + +Group roles can change as responsibilities evolve. Click the role dropdown next to any member to toggle them between Member and Admin. Role changes take effect immediately. + +Promoting members to admin grants them access to group management capabilities. They'll see a "Group Management" link in their interface and can add/remove members, create tags, and manage the group independently. This distributes administrative workload and empowers group leads. + +Demoting admins to members removes their management capabilities but preserves their group membership. They retain access to group tags and recordings but can no longer manage group membership or tags. + +### Removing Group Members + +Click the red user-times icon next to any member to remove them from the group. Removal is immediate and has several effects: + +- The user loses access to group tags +- They won't receive new group-tagged recordings +- Their access to previously-shared group recordings persists +- Their personal notes on group recordings are preserved + +If you need to fully revoke access to existing group recordings, you must manually revoke those internal shares through the recording's share management interface. Group removal only prevents future automatic sharing. + +### Deleting Groups + +Click the red trash icon next to a group to delete it entirely. A confirmation dialog prevents accidental deletion. Deleting a group: + +- Removes all group memberships +- Deletes all group tags (via database cascade) +- Preserves all recordings (including previously group-tagged ones) +- Preserves all internal shares created by group tags + +Group deletion is irreversible. Once deleted, the group structure is gone, though recordings and access permissions created by the group persist. If you need to temporarily disable a group, consider removing all members instead of deleting the group itself. + +## Group Tags + +Group tags power automatic sharing within groups. Unlike personal tags that organize individual content, group tags trigger access grants across all group members whenever applied. + +### Creating Group Tags + +From the Groups tab, click the purple tags icon next to the relevant group. This opens the group tags modal showing existing tags and a creation form. + +Provide a tag name that describes the content type or purpose. Good names are specific and clear: "Sprint Reviews", "Customer Calls", "Legal Contracts". Avoid generic names like "Important" or "Group" that don't convey useful information. + +Select a color to visually distinguish the tag. Colors help users quickly identify content categories in the interface. Consider establishing color conventions - blue for technical content, green for sales, red for legal, etc. + +### Tag Retention Policies + +Group tags can override global retention settings with tag-specific retention periods. This powerful feature lets different content types have different lifecycles within the same instance. + +Leave the retention field empty to use global retention settings. The tag won't affect how long recordings are kept - they'll follow the instance-wide `GLOBAL_RETENTION_DAYS` setting. + +Enter a number of days to set custom retention for this tag. Recordings with this tag will be auto-deleted after the specified period, regardless of global settings. For example: + +- Legal group: 2555 days (7 years) for contracts and compliance recordings +- Operations group: 14 days for daily stand-ups +- Marketing group: 180 days for campaign planning sessions + +When a recording has multiple tags with different retention periods, the shortest period applies. This ensures content is never kept longer than its most restrictive tag allows. + +### Protection from Deletion + +Enable "Protect from deletion" to make recordings with this tag immune to automatic deletion. Protected recordings are never auto-deleted regardless of age, global retention settings, or other tag retention periods. + +Use protection for recordings that must be permanently preserved: + +- Legal and compliance records +- Critical business decisions +- Training and onboarding materials +- Reference documentation +- Historical archives + +Protection can be removed by editing the tag later if preservation requirements change. Removing protection doesn't immediately delete recordings - they'll be evaluated for deletion on the next retention check based on their age and other applicable retention policies. + +### Auto-Share Settings + +Group tags support two levels of automatic sharing that trigger when any group member applies the tag to a recording: + +**Share with All Group Members** is the default and recommended approach. When enabled, applying this tag shares the recording with every group member (excluding the owner). All members receive view and edit permissions, enabling full collaboration. + +**Share with Group Leads Only** restricts automatic sharing to group admins. When enabled, only users with the admin role in this group receive automatic access. Regular members don't get automatic access, though group admins can manually share with them if needed. This option suits sensitive content that requires administrative oversight before wider distribution. + +Both options can be enabled simultaneously, though this is redundant - sharing with all members already includes group leads. Use one or the other based on your content sensitivity and group structure. + +### Managing Group Tags + +Existing group tags appear in the group tags modal with their current settings. Click the edit icon to modify a tag's name, color, retention, protection, or sharing settings. Changes affect the tag going forward but don't retroactively change already-applied tags or shares. + +Delete group tags by clicking the trash icon. Deleted tags: + +- Are removed from all recordings they were applied to +- Disappear from tag selectors for all group members +- Don't delete the recordings themselves +- Don't revoke access already granted through the tag + +If a tag was widely used, consider the impact before deletion. Users may have organized content around that tag, and deletion removes that organizational structure. In most cases, retaining unused tags causes no harm. + +### Syncing Group Shares + +If your instance enabled group features after recordings were already tagged, or if group membership changed significantly, you might have group-tagged recordings that weren't automatically shared with current group members. The "Sync Group Shares" feature addresses this. + +Click "Sync Group Shares" in the group management modal to open the sync dialog. Review the information about what the sync will do - it applies automatic sharing retroactively to all existing recordings with this group's tags. + +The sync operation: + +- Identifies all recordings tagged with any of this group's tags +- Checks each recording for existing shares with current group members +- Creates missing shares for group members who should have access but don't +- Respects the tag's sharing settings (all members vs. group leads only) +- Skips recordings where members already have access + +Confirm the sync to execute. Depending on the number of tagged recordings and group size, this might take a few seconds to several minutes. A result modal shows how many shares were created and how many recordings were processed. + +Sync is safe to run multiple times - it won't create duplicate shares. Use it after adding many new members, after fixing misconfigured tags, or when migrating from older Speakr versions that didn't have full group support. + +## Group Admin Role + +Group admins are group members with elevated permissions within their group's scope. Unlike full instance administrators who can manage all groups and system settings, group admins can only manage groups where they have the admin role. + +### Granting Group Admin Access + +When adding a member to a group, select "Admin" from the role dropdown. The user immediately gains group admin capabilities for that group only. They cannot manage other groups or access system-wide administrative features. + +Group admins see a "Group Management" link in their user menu instead of the full admin link. Clicking this takes them to a focused admin interface showing only groups they administer. The interface is identical to the Groups tab regular admins see, but scoped to their groups. + +### Group Admin Capabilities + +Group admins can perform these actions within their groups: + +- Add new members from the instance's user base +- Remove existing members (excluding themselves) +- Change member roles between admin and member +- Create new group tags with full configuration options +- Edit existing group tags including retention and sharing settings +- Delete group tags +- Sync group shares for their groups + +Group admins cannot: + +- Create new groups +- Delete groups +- Manage groups they're not admins of +- Access system-wide admin features (users, settings, statistics) +- Grant themselves admin access to other groups + +This scoped access lets you distribute group management responsibility to group leads without granting full administrative access. Group leads can manage their groups independently while you maintain control over instance-wide settings and group creation. + +### Security Boundaries + +Group admins have powerful capabilities within their groups but cannot escalate their privileges. They cannot: + +- Make themselves full instance administrators +- Grant themselves admin roles in other groups +- Access or modify system settings +- View statistics for other groups or the entire instance +- Delete recordings owned by other users (even within their group) + +The database enforces these boundaries at the API level. Even if a group admin could somehow call instance-wide admin APIs, the backend verifies permissions and rejects unauthorized requests. The UI simply hides controls group admins can't use, but security doesn't rely on UI hiding. + +## Configuration Reference + +### Environment Variables + +```bash +# Internal Sharing (Required) +ENABLE_INTERNAL_SHARING=true|false + +# Username Display +SHOW_USERNAMES_IN_UI=true|false + +# Public Sharing Control (Affects group members' public sharing) +ENABLE_PUBLIC_SHARING=true|false + +# Retention Settings (Groups can override) +ENABLE_AUTO_DELETION=true|false +GLOBAL_RETENTION_DAYS=90 +DELETION_MODE=audio_only|full_recording +``` + +### Database Schema + +Groups use several database tables that work together: + +**Group Table**: + +- `id`: Primary key +- `name`: Group name (max 100 chars) +- `description`: Optional group description +- `created_by`: User ID of creator (full admin) +- `created_at`: Creation timestamp + +**TeamMembership Table**: + +- `id`: Primary key +- `team_id`: References Group +- `user_id`: References User +- `role`: "admin" or "member" +- `joined_at`: Membership timestamp + +**Tag Table** (Extended): + +- `team_id`: References Group (null for personal tags) +- `retention_days`: Custom retention override (null uses global) +- `protect_from_deletion`: Boolean protection flag +- `auto_share_on_apply`: Boolean (share with all members) +- `share_with_team_lead`: Boolean (share with group admins only) + +Cascade deletion is configured so deleting a group deletes its tags and memberships, but preserves recordings and shares. + +## Troubleshooting + +### Groups Tab Not Visible + +**Cause**: Internal sharing not enabled or not configured correctly. + +**Solution**: + +1. Check `.env` contains `ENABLE_INTERNAL_SHARING=true` +2. Restart Speakr after `.env` changes +3. Clear browser cache and reload +4. Check application logs for startup errors + +### Users Can't See Group Tags + +**Cause**: User not added to group, or internal sharing disabled. + +**Solution**: + +1. Verify user is listed in group membership +2. Confirm `ENABLE_INTERNAL_SHARING=true` in `.env` +3. Check user is logged in (group tags hidden for anonymous users) +4. Refresh the page to load updated tag lists + +### Auto-Sharing Not Working + +**Cause**: Group tag misconfigured or internal sharing disabled. + +**Solution**: + +1. Edit the group tag and verify "Share with all group members" or "Share with group leads" is enabled +2. Confirm `ENABLE_INTERNAL_SHARING=true` in `.env` +3. Check application logs when applying tags for sharing errors +4. Try manually sharing the recording to verify sharing infrastructure works + +### Group Admin Can't Access Admin Interface + +**Cause**: User doesn't have admin role in any group, or routing issue. + +**Solution**: + +1. Verify user role is "admin" not "member" in group membership +2. Have user log out and back in to refresh session +3. Check "Group Management" link appears in user menu (not "Admin") +4. Review application logs for permission errors + +### Recordings Not Deleted Per Retention Policy + +**Cause**: Protected tags, misconfigured retention, or auto-deletion disabled. + +**Solution**: + +1. Check if recording has protected group tags +2. Verify `ENABLE_AUTO_DELETION=true` in `.env` +3. Confirm `GLOBAL_RETENTION_DAYS` is set if no tag retention applies +4. Review cron scheduler logs for deletion errors +5. Check tag retention_days is set correctly (null = use global) + +### Sync Group Shares Shows Zero Shares Created + +**Cause**: All applicable shares already exist, or no recordings have group tags. + +**Solution**: + +1. Verify recordings actually have tags from this group +2. Check if group members already have access via other shares +3. Review whether recordings are owned by current group members (no self-sharing) +4. Confirm group has members beyond the recording owners + +## Best Practices + +### Group Structure + +**Small Organizations (<10 users)**: +Create groups per department (Engineering, Sales, HR). Use group tags for project names or content types. Liberal use of groups promotes collaboration since everyone knows everyone. + +**Large Organizations (>10 users)**: +Create groups per product, division, or major project. Use nested organizational patterns if needed (separate groups for Product A Engineering and Product A Sales). More selective group membership prevents information overload. + +### Tag Naming Conventions + +Establish conventions early and document them for consistency: + +``` +Project-Based: "Project-Phoenix", "Initiative-Q3-2024" +Content-Type: "Sprint-Reviews", "Customer-Calls", "Legal-Contracts" +Department: "Eng-Architecture", "Sales-Training", "HR-Interviews" +``` + +Avoid generic names that don't communicate purpose: +❌ "Important", "Misc", "Other", "Temp", "Group" +✓ "Executive-Briefings", "Tech-Specs", "Client-Demos" + +### Retention Strategy + +Set thoughtful defaults that balance storage costs with compliance needs: + +``` +Global Default: 90 days (captures most content) +Legal Group Tags: 2555 days (7 years for legal records) +Compliance Tags: Protected (permanent retention) +Meeting Tags: 180 days (reasonable collaboration window) +Stand-up Tags: 14 days (ephemeral daily content) +``` + +Review retention policies quarterly to ensure they remain appropriate as business needs change. + +### Group Admin Distribution + +Grant group admin roles to natural group leaders - project managers, department heads, tech leads. This distributes administrative workload and empowers groups to self-manage. + +Avoid granting group admin too liberally. While it's scoped to individual groups, group admins can add members and create tags that affect access. Limit the role to trusted individuals who understand security implications. + +Document each group's admins in the group description or external documentation. Future administrators will appreciate knowing who to contact about group-specific questions. + +## Integration with Other Features + +### Inquire Mode + +Group tags automatically appear in Inquire Mode's available filters, enabling group-scoped semantic search. Users can search for "budget discussions" and filter to just their project group, finding relevant conversations without noise from other groups. + +Recordings shared via group tags are included in semantic search results. The vector store indexes all accessible recordings, meaning group content becomes part of users' searchable knowledge base automatically. + +### Retention and Auto-Deletion + +Tag-level retention policies integrate with Speakr's auto-deletion system. The nightly retention check evaluates each recording's tags to determine applicable retention periods: + +1. If recording has protected tags → Never deleted +2. If recording has tags with `retention_days` → Use shortest tag retention +3. Otherwise → Use global `GLOBAL_RETENTION_DAYS` + +This cascading system lets groups set specific policies while maintaining instance-wide defaults for untagged content. + +### Public Sharing + +Group membership doesn't affect public sharing capabilities. Users' ability to create public share links is controlled by: + +1. Global `ENABLE_PUBLIC_SHARING` setting +2. Per-user `can_share_publicly` permission (if global is enabled) + +Group members can create public links for group recordings if they have appropriate permissions, enabling external stakeholder communication while maintaining group-internal collaboration. + +--- + +Groups transform multi-user Speakr instances into collaborative platforms where information flows automatically to relevant people. Proper configuration and management ensure security while enabling seamless knowledge sharing. + +For user-focused group documentation, see the [Group Collaboration](../user-guide/groups.md) guide. + +Return to [Admin Guide](index.md) → diff --git a/docs/admin-guide/index.md b/docs/admin-guide/index.md new file mode 100644 index 00000000..ca37d14b --- /dev/null +++ b/docs/admin-guide/index.md @@ -0,0 +1,142 @@ +# Admin Guide + +Welcome to the Speakr Admin Guide! As an administrator, you control the heart of your Speakr instance, managing users, monitoring system health, and configuring AI behavior. + +## Administrative Controls + +
+
+
👥
+

User Management

+

Create accounts, manage permissions, monitor usage, and control access to your Speakr instance.

+ Manage Users → +
+ +
+
🤝
+

Group Management

+

Create groups, assign roles, configure auto-sharing tags, and enable organized collaboration.

+ Manage Groups → +
+ +
+
📊
+

System Statistics

+

Monitor system health, track usage patterns, and identify potential issues before they affect users.

+ View Statistics → +
+ +
+
🔧
+

System Settings

+

Configure global limits, timeouts, file sizes, and system-wide behavior that affects all users.

+ Configure System → +
+ +
+
🤖
+

Model Configuration

+

Configure AI models for text generation, including GPT-5 support and provider selection.

+ Configure Models → +
+ +
+
+

Default Prompts

+

Customize AI behavior with default summary prompts that shape how content is processed.

+ Set Prompts → +
+ +
+
🔍
+

Vector Store

+

Manage semantic search capabilities, monitor embedding status, and control Inquire Mode.

+ Manage Search → +
+ +
+
🗑️
+

Retention & Auto-Deletion

+

Configure automated data lifecycle management with flexible retention policies and smart deletion rules.

+ Manage Retention → +
+ +
+
📧
+

Email Setup

+

Configure email verification for new registrations and enable password reset functionality.

+ Setup Email → +
+ +
+
🔐
+

SSO Setup

+

Integrate with identity providers like Keycloak, Azure AD, Google, or Auth0 using OpenID Connect.

+ Configure SSO → +
+
+ +## Quick Actions + +
+
+ +
+ Add New User +

User Management → Add User Button → Enter details → Set permissions

+
+
+ +
+ 🤝 +
+ Create a Group +

Group Management → Create Group → Add members → Configure group tags

+
+
+ +
+ 📈 +
+ Check System Health +

System Statistics → Review metrics → Check processing status → Monitor storage

+
+
+ +
+ ⚙️ +
+ Update Settings +

System Settings → Adjust limits → Configure timeouts → Save changes

+
+
+ +
+ 🔄 +
+ Process Embeddings +

Vector Store → Check status → Process pending → Monitor progress

+
+
+
+ +## Need Admin Help? + +
+
+ 📖 + Review the detailed Troubleshooting Guide +
+
+ 🐛 + Check Docker logs: docker compose logs -f app +
+
+ 💾 + Backup your data directory regularly +
+
+ +--- + +Ready to manage your Speakr instance? Start with [User Management](user-management.md) → \ No newline at end of file diff --git a/docs/admin-guide/migration-guide.md b/docs/admin-guide/migration-guide.md new file mode 100644 index 00000000..50af4c6a --- /dev/null +++ b/docs/admin-guide/migration-guide.md @@ -0,0 +1,245 @@ +# Migration Guide: Connector Architecture + +This guide helps you migrate from the legacy transcription configuration to the new connector-based architecture introduced in Speakr v0.8. + +## Overview + +Speakr now uses a **connector-based architecture** for transcription services. This provides: + +- **Simplified configuration** - Fewer environment variables needed +- **Auto-detection** - Speakr can attempt to automatically select the right connector +- **Better feature support** - Data-driven UI that adapts to connector capabilities +- **Extensibility** - Possibility to add custom connectors for new providers + +## Backwards Compatibility + +**Your existing configuration will continue to work.** The new architecture maintains full backwards compatibility with legacy environment variables. However, you may see deprecation warnings in the logs for certain settings. + +## What's Changed + +### Deprecated Environment Variables + +| Deprecated Variable | Status | Migration | +|---------------------|--------|-----------| +| `USE_ASR_ENDPOINT=true` | Still works, logs warning | Just set `ASR_BASE_URL` instead | +| `WHISPER_MODEL` | Still works, logs warning | Use `TRANSCRIPTION_MODEL` instead | + +### New Environment Variables + +| Variable | Description | +|----------|-------------| +| `TRANSCRIPTION_CONNECTOR` | Explicit connector selection (optional, auto-detected) | +| `TRANSCRIPTION_MODEL` | Model name for OpenAI connectors | + +### Auto-Detection Priority + +Speakr automatically selects a connector based on your configuration: + +1. **Explicit selection** - If `TRANSCRIPTION_CONNECTOR` is set, use that connector +2. **ASR mode** - If `ASR_BASE_URL` is set, use the ASR Endpoint connector +3. **OpenAI Transcribe** - If `TRANSCRIPTION_MODEL` contains `gpt-4o`, use OpenAI Transcribe connector +4. **Default** - Use OpenAI Whisper connector with `TRANSCRIPTION_MODEL` or `whisper-1` + +## Migration Examples + +### From Legacy ASR Configuration + +**Before (Legacy):** +```bash +USE_ASR_ENDPOINT=true +ASR_BASE_URL=http://whisperx-asr:9000 +ASR_DIARIZE=true +ASR_RETURN_SPEAKER_EMBEDDINGS=true +``` + +**After (New - Minimal):** +```bash +ASR_BASE_URL=http://whisperx-asr:9000 +ASR_RETURN_SPEAKER_EMBEDDINGS=true +``` + +The `USE_ASR_ENDPOINT=true` is no longer needed—setting `ASR_BASE_URL` automatically enables ASR mode. Diarization is enabled by default for ASR endpoints. + +### From Legacy Whisper Configuration + +**Before (Legacy):** +```bash +TRANSCRIPTION_BASE_URL=https://api.openai.com/v1 +TRANSCRIPTION_API_KEY=sk-xxx +WHISPER_MODEL=whisper-1 +``` + +**After (New):** +```bash +TRANSCRIPTION_API_KEY=sk-xxx +TRANSCRIPTION_MODEL=whisper-1 +``` + +The base URL defaults to OpenAI's API, and `TRANSCRIPTION_MODEL` replaces the deprecated `WHISPER_MODEL`. + +### Upgrading to OpenAI Diarization + +If you want speaker diarization without running a self-hosted ASR service: + +**New Configuration:** +```bash +TRANSCRIPTION_API_KEY=sk-xxx +TRANSCRIPTION_MODEL=gpt-4o-transcribe-diarize +``` + +This uses OpenAI's built-in diarization. The connector is auto-detected from the model name. + +### Using Mistral Voxtral + +Mistral's Voxtral provides cloud-based transcription with diarization: + +```bash +TRANSCRIPTION_CONNECTOR=mistral +TRANSCRIPTION_API_KEY=your-mistral-key +TRANSCRIPTION_MODEL=voxtral-mini-latest +``` + +### Using VibeVoice ASR (Self-Hosted) + +VibeVoice runs on your own hardware via vLLM, with no cloud dependency: + +```bash +TRANSCRIPTION_CONNECTOR=vibevoice +TRANSCRIPTION_BASE_URL=http://your-vllm-server:8000 +TRANSCRIPTION_MODEL=vibevoice +``` + +Both connectors support speaker diarization, timestamps, and automatic language detection. + +## Chunking Behavior Changes + +The new architecture makes chunking **connector-aware**: + +| Connector | Chunking Behavior | +|-----------|-------------------| +| **ASR Endpoint** | Handled internally—your `CHUNK_*` settings are ignored | +| **OpenAI Transcribe** | Handled internally via `chunking_strategy=auto`—your settings are ignored | +| **Mistral** | Handled internally—your `CHUNK_*` settings are ignored | +| **VibeVoice** | App chunks files over ~58 minutes into ~50 minute pieces automatically | +| **OpenAI Whisper** | Uses your `CHUNK_LIMIT` and `CHUNK_OVERLAP_SECONDS` settings | + +If you were manually configuring chunking for ASR endpoints, you can remove those settings as they no longer have any effect. + +## UI Feature Changes + +Some UI features are now **data-driven** rather than configuration-driven: + +| Feature | Old Behavior | New Behavior | +|---------|--------------|--------------| +| Speaker identification button | Shown when `USE_ASR_ENDPOINT=true` | Shown when transcription has diarization data | +| Min/Max speakers in reprocess | Always shown for ASR | Only shown when connector supports it | +| Bubble view toggle | Based on config | Based on whether transcription has dialogue | + +This means features automatically appear when available, regardless of which connector produced the transcription. + +## Verifying Your Migration + +After updating your configuration: + +1. **Check the logs** - Look for deprecation warnings: + ```bash + docker compose logs app | grep -i deprecat + ``` + +2. **Test transcription** - Upload a test file and verify it transcribes correctly + +3. **Check system info** - Visit `/api/system/info` to see the active connector: + ```json + { + "transcription": { + "connector": "asr_endpoint", + "supports_diarization": true, + "supports_speaker_embeddings": true + } + } + ``` + +## Recommended Configuration + +### For Mistral Voxtral (Cloud Diarization) + +```bash +# Transcription +TRANSCRIPTION_CONNECTOR=mistral +TRANSCRIPTION_API_KEY=your-mistral-key +TRANSCRIPTION_MODEL=voxtral-mini-latest + +# Text generation +TEXT_MODEL_BASE_URL=https://openrouter.ai/api/v1 +TEXT_MODEL_API_KEY=sk-or-v1-xxx +TEXT_MODEL_NAME=openai/gpt-4o-mini +``` + +### For VibeVoice ASR (Self-Hosted, No Cloud) + +```bash +# Transcription +TRANSCRIPTION_CONNECTOR=vibevoice +TRANSCRIPTION_BASE_URL=http://your-vllm-server:8000 +TRANSCRIPTION_MODEL=vibevoice + +# Text generation +TEXT_MODEL_BASE_URL=https://openrouter.ai/api/v1 +TEXT_MODEL_API_KEY=sk-or-v1-xxx +TEXT_MODEL_NAME=openai/gpt-4o-mini +``` + +### For Self-Hosted (Best Quality) + +Using WhisperX ASR Service for superior transcription and diarization: + +```bash +# Transcription +ASR_BASE_URL=http://whisperx-asr:9000 +ASR_RETURN_SPEAKER_EMBEDDINGS=true + +# Text generation +TEXT_MODEL_BASE_URL=https://openrouter.ai/api/v1 +TEXT_MODEL_API_KEY=sk-or-v1-xxx +TEXT_MODEL_NAME=openai/gpt-4o-mini +``` + +### For Cloud-Based (No Self-Hosting) + +Using OpenAI's transcription with diarization: + +```bash +# Transcription +TRANSCRIPTION_API_KEY=sk-xxx +TRANSCRIPTION_MODEL=gpt-4o-transcribe-diarize + +# Text generation +TEXT_MODEL_BASE_URL=https://openrouter.ai/api/v1 +TEXT_MODEL_API_KEY=sk-or-v1-xxx +TEXT_MODEL_NAME=openai/gpt-4o-mini +``` + +## Troubleshooting + +### "Connector not found" Error + +Ensure you have the correct environment variables set. Check the auto-detection priority above. + +### Features Missing After Migration + +If UI features like speaker identification are missing: + +- Verify the transcription actually contains diarization data +- Check that your connector supports the feature (e.g., voice profiles require ASR endpoint) + +### Deprecation Warnings in Logs + +These are informational only—your configuration still works. Update your `.env` file at your convenience to use the new variable names. + +## Getting Help + +If you encounter issues during migration: + +1. Check the [troubleshooting guide](../troubleshooting.md) +2. Review the [installation guide](../getting-started/installation.md) for complete configuration examples +3. Open an issue on [GitHub](https://github.com/murtaza-nasir/speakr/issues) diff --git a/docs/admin-guide/model-configuration.md b/docs/admin-guide/model-configuration.md new file mode 100644 index 00000000..94ce6fb2 --- /dev/null +++ b/docs/admin-guide/model-configuration.md @@ -0,0 +1,542 @@ +# Model Configuration + +This guide covers how to configure AI models for text generation in Speakr, including support for OpenAI's GPT-5 series and other language models. + +## Overview + +Speakr uses AI models for several key features: + +- **Summary Generation**: Creating intelligent summaries of your transcriptions +- **Title Generation**: Automatically generating descriptive titles for recordings +- **Event Extraction**: Identifying calendar-worthy events from conversations +- **Interactive Chat**: Answering questions about your recordings +- **Speaker Identification**: Detecting speaker names from conversation context + +These features are powered by large language models (LLMs) configured through your `.env` file. + +## Basic Configuration + +The text generation model is configured using three environment variables: + +```bash +TEXT_MODEL_BASE_URL=https://openrouter.ai/api/v1 +TEXT_MODEL_API_KEY=your_api_key_here +TEXT_MODEL_NAME=openai/gpt-4o-mini +``` + +### Choosing a Provider + +**OpenRouter** (recommended for most users): Provides access to multiple AI models through a single API, often at competitive prices. Supports GPT-4, Claude, and many other models. Configure using `TEXT_MODEL_BASE_URL=https://openrouter.ai/api/v1`. + +**OpenAI Direct**: Use OpenAI's API directly for access to their latest models including GPT-5. Configure using `TEXT_MODEL_BASE_URL=https://api.openai.com/v1`. This option is required for GPT-5 models with their specialized parameters. + +**Custom Endpoints**: Speakr works with any OpenAI-compatible API endpoint, including self-hosted solutions like LocalAI, Ollama with OpenAI compatibility, or enterprise API gateways. + +**Google Gemini (OpenAI-compatible)**: Google exposes Gemini models behind an OpenAI-compatible URL. Point Speakr at it like any other base URL: + +```bash +TEXT_MODEL_BASE_URL=https://generativelanguage.googleapis.com/v1beta/openai/ +TEXT_MODEL_API_KEY=your_google_api_key +TEXT_MODEL_NAME=gemini-2.0-flash +``` + +No special connector is needed; Speakr's standard LLM client works with the endpoint directly. + +## GPT-5 Support + +Speakr fully supports OpenAI's GPT-5 model family, automatically detecting and adjusting API parameters when you use GPT-5 models with the official OpenAI API. + +### Requirements + +- **OpenAI Python SDK**: Version 2.2.0 or higher (included in `requirements.txt`) +- **OpenAI API**: Must use `TEXT_MODEL_BASE_URL=https://api.openai.com/v1` +- **Valid API Key**: An OpenAI API key with GPT-5 access + +### Supported GPT-5 Models + +- **gpt-5**: Best for complex reasoning, broad world knowledge, and code-heavy tasks +- **gpt-5-mini**: Cost-optimized reasoning and chat; balances speed, cost, and capability +- **gpt-5-nano**: High-throughput tasks, especially simple instruction-following +- **gpt-5-chat-latest**: Latest GPT-5 chat model + +### Key Differences from GPT-4 + +GPT-5 models use different parameters than previous models: + +**Unsupported Parameters** (will cause errors if used): + +- `temperature` - Replaced by `reasoning_effort` and `verbosity` +- `top_p` - Not supported +- `logprobs` - Not supported + +**New GPT-5 Parameters**: + +**Reasoning Effort**: Controls how many reasoning tokens the model generates before producing a response. + +- **minimal**: Fastest responses, minimal reasoning tokens (best for simple tasks) +- **low**: Fast responses with basic reasoning +- **medium**: Balanced reasoning and speed (default, recommended) +- **high**: Maximum reasoning for complex tasks like coding and multi-step planning + +**Verbosity**: Controls how many output tokens are generated. + +- **low**: Concise responses +- **medium**: Balanced detail (default) +- **high**: Thorough explanations and detailed code + +**Token Limits**: GPT-5 uses `max_completion_tokens` instead of `max_tokens`. + +### Configuring GPT-5 + +Add these settings to your `.env` file: + +```bash +# Use OpenAI API endpoint +TEXT_MODEL_BASE_URL=https://api.openai.com/v1 +TEXT_MODEL_API_KEY=your_openai_api_key +TEXT_MODEL_NAME=gpt-5-mini + +# GPT-5 specific parameters (optional, defaults shown) +GPT5_REASONING_EFFORT=medium +GPT5_VERBOSITY=medium +``` + +### GPT-5 Configuration Examples + +**Fast Summarization (Low Cost)**: +```bash +TEXT_MODEL_NAME=gpt-5-nano +GPT5_REASONING_EFFORT=minimal +GPT5_VERBOSITY=low +``` + +**Standard Usage (Recommended)**: +```bash +TEXT_MODEL_NAME=gpt-5-mini +GPT5_REASONING_EFFORT=medium +GPT5_VERBOSITY=medium +``` + +**Complex Analysis (High Quality)**: +```bash +TEXT_MODEL_NAME=gpt-5 +GPT5_REASONING_EFFORT=high +GPT5_VERBOSITY=high +``` + +### Automatic Detection + +Speakr automatically detects when you're using: + +1. A GPT-5 model (based on model name) +2. The official OpenAI API (based on base URL containing `api.openai.com`) + +When both conditions are met, Speakr automatically: + +- Removes `temperature` parameter from API calls +- Adds `reasoning_effort` parameter +- Adds `verbosity` parameter +- Uses `max_completion_tokens` instead of `max_tokens` +- Logs that GPT-5 parameters are being used + +Check your logs for confirmation: +``` +Using GPT-5 model: gpt-5-mini - applying GPT-5 specific parameters +``` + +### Using GPT-5 Through OpenRouter + +If you use GPT-5 models through OpenRouter or other proxy services, the automatic GPT-5 parameter handling will **not** activate. These services typically handle parameter translation themselves, so Speakr uses standard parameters (temperature, max_tokens, etc.). + +### Use Cases + +**Summarization**: + +- Fast summaries: `gpt-5-nano` with `minimal` effort and `low` verbosity +- Standard summaries: `gpt-5-mini` with `medium` effort and `medium` verbosity +- Detailed summaries: `gpt-5` with `medium` effort and `high` verbosity + +**Chat**: + +- Quick Q&A: `gpt-5-mini` with `minimal` effort and `low` verbosity +- Standard conversation: `gpt-5-mini` with `low` effort and `medium` verbosity +- Complex analysis: `gpt-5` with `high` effort and `medium` verbosity + +### Troubleshooting GPT-5 + +**Error: "Unsupported parameter 'temperature'"** + +This means GPT-5 detection failed. Check that: + +1. `TEXT_MODEL_BASE_URL` contains `api.openai.com` +2. `TEXT_MODEL_NAME` starts with `gpt-5` or is one of: `gpt-5`, `gpt-5-mini`, `gpt-5-nano`, `gpt-5-chat-latest` + +**Error: "Invalid reasoning_effort value"** + +Valid values are: `minimal`, `low`, `medium`, `high` + +**Error: "Invalid verbosity value"** + +Valid values are: `low`, `medium`, `high` + +### Migrating from GPT-4 to GPT-5 + +1. **Update dependencies** (required for GPT-5): + ```bash + pip install -r requirements.txt + ``` + This upgrades the OpenAI SDK to version 2.2.0 or higher. + +2. Update your `.env` file: + ```bash + TEXT_MODEL_NAME=gpt-5-mini # or gpt-5, gpt-5-nano + ``` + +3. Add GPT-5 parameters (optional): + ```bash + GPT5_REASONING_EFFORT=medium + GPT5_VERBOSITY=medium + ``` + +4. Restart Speakr: + ```bash + docker compose restart + ``` + +5. Check logs for confirmation: + ``` + Using GPT-5 model: gpt-5-mini - applying GPT-5 specific parameters + ``` + +### Performance Considerations + +- **Cost**: `gpt-5-nano` < `gpt-5-mini` < `gpt-5` +- **Speed**: `minimal` < `low` < `medium` < `high` reasoning effort +- **Quality**: Generally increases with model size and reasoning effort +- **Token usage**: Higher verbosity = more output tokens + +For most use cases, we recommend: + +- **Model**: `gpt-5-mini` +- **Reasoning**: `medium` +- **Verbosity**: `medium` + +This provides a good balance of cost, speed, and quality. + +## Separate Chat Model Configuration + +Speakr allows you to configure a separate model specifically for real-time chat interactions, while using a different model for background tasks like summarization and title generation. This enables you to: + +- **Use different service tiers**: Configure a faster, more expensive model for interactive chat while using a cheaper model for background processing +- **Optimize costs**: Use a budget-friendly model for summarization while keeping a high-quality model for chat +- **Balance speed and quality**: Prioritize low latency for chat while accepting slower processing for summaries + +### Configuration + +Add these optional environment variables to your `.env` file: + +```bash +# Chat Model Configuration (Optional) +# If not set, chat will use TEXT_MODEL_* settings +CHAT_MODEL_API_KEY=your_chat_api_key +CHAT_MODEL_BASE_URL=https://openrouter.ai/api/v1 +CHAT_MODEL_NAME=openai/gpt-4o +``` + +### Fallback Behavior + +| Configuration | Behavior | +|--------------|----------| +| No `CHAT_MODEL_*` variables set | Chat uses `TEXT_MODEL_*` settings (default) | +| Only `CHAT_MODEL_NAME` set | Falls back to `TEXT_MODEL_*` (API key required) | +| Only `CHAT_MODEL_API_KEY` set | Falls back to `TEXT_MODEL_*` (model name required) | +| `CHAT_MODEL_API_KEY` + `CHAT_MODEL_NAME` set | Uses chat config with `TEXT_MODEL_BASE_URL` | +| All `CHAT_MODEL_*` variables set | Uses fully dedicated chat configuration | + +### GPT-5 Settings for Chat + +If you use GPT-5 models for chat, you can configure separate GPT-5 parameters: + +```bash +# Chat-specific GPT-5 settings (optional) +# Falls back to GPT5_* settings if not specified +CHAT_GPT5_REASONING_EFFORT=medium +CHAT_GPT5_VERBOSITY=medium +``` + +### Example Configurations + +**Cheap Summarization + Premium Chat**: +```bash +# Background tasks: Use budget model +TEXT_MODEL_BASE_URL=https://openrouter.ai/api/v1 +TEXT_MODEL_API_KEY=your_openrouter_key +TEXT_MODEL_NAME=openai/gpt-4o-mini + +# Interactive chat: Use premium model +CHAT_MODEL_API_KEY=your_openai_key +CHAT_MODEL_BASE_URL=https://api.openai.com/v1 +CHAT_MODEL_NAME=gpt-5-mini +CHAT_GPT5_REASONING_EFFORT=low +CHAT_GPT5_VERBOSITY=medium +``` + +**Same Provider, Different Models**: +```bash +# Background tasks: Smaller model +TEXT_MODEL_BASE_URL=https://openrouter.ai/api/v1 +TEXT_MODEL_API_KEY=your_api_key +TEXT_MODEL_NAME=google/gemini-2.5-flash-lite + +# Interactive chat: Larger model (same provider) +CHAT_MODEL_NAME=openai/gpt-4o +# Note: CHAT_MODEL_API_KEY not needed if using same key +# Note: CHAT_MODEL_BASE_URL not needed if using same endpoint +``` + +**Different Service Tiers (OpenAI)**: +```bash +# Background tasks: Standard tier +TEXT_MODEL_BASE_URL=https://api.openai.com/v1 +TEXT_MODEL_API_KEY=your_standard_tier_key +TEXT_MODEL_NAME=gpt-4o-mini + +# Interactive chat: Priority tier for faster responses +CHAT_MODEL_API_KEY=your_priority_tier_key +CHAT_MODEL_NAME=gpt-4o +``` + +### When to Use Separate Chat Models + +**Recommended for**: + +- High-volume deployments where chat responsiveness is critical +- Users who need different service tiers for different operations +- Cost optimization when chat usage is significantly higher than summarization + +**Not needed for**: + +- Small deployments with low usage +- When using the same model for all operations is acceptable +- Simple setups where configuration simplicity is preferred + +## Model Selection Guidelines + +### For Summaries + +The model you choose significantly impacts summary quality: + +- **GPT-4 or better**: Produces nuanced, context-aware summaries with excellent understanding of complex topics +- **GPT-5-mini**: Excellent balance of quality and cost for most summarization needs +- **GPT-3.5/4o-mini**: Budget-friendly option, suitable for straightforward content +- **Claude models**: Strong performance on structured content and technical material + +### For Chat + +Chat features benefit from more capable models: + +- **GPT-5**: Best for complex multi-turn conversations and detailed analysis +- **GPT-5-mini**: Recommended for most chat use cases +- **Claude**: Excellent for technical discussions and code-related queries + +### Cost Optimization + +To reduce costs while maintaining quality: + +1. **Use smaller models for simple tasks**: `gpt-5-nano` or `gpt-4o-mini` handle straightforward summaries well +2. **Adjust GPT-5 reasoning effort**: Use `minimal` or `low` for quick tasks +3. **Set token limits**: Configure `SUMMARY_MAX_TOKENS` and `CHAT_MAX_TOKENS` in your `.env` +4. **Use OpenRouter**: Often provides better rates than direct API access + +### Testing Configuration + +After changing model configuration: + +1. Restart the Speakr container +2. Create a test recording +3. Review the generated summary and title +4. Test the chat feature +5. Monitor logs for any errors or warnings + +## Environment Variables Reference + +```bash +# Required: API endpoint +TEXT_MODEL_BASE_URL=https://api.openai.com/v1 + +# Required: API key +TEXT_MODEL_API_KEY=your_api_key_here + +# Required: Model identifier +TEXT_MODEL_NAME=gpt-5-mini + +# Optional: Maximum tokens for summaries (default: 8000) +SUMMARY_MAX_TOKENS=8000 + +# Optional: Maximum tokens for chat responses (default: 2000) +CHAT_MAX_TOKENS=2000 + +# Optional: Maximum tokens for AI title generation (default: 100) +# Bump for reasoning models (o1, Kimi 2.5, etc.) that consume budget on hidden thinking tokens +TITLE_MAX_TOKENS=200 + +# Optional: Maximum tokens for event extraction from transcripts (default: 4000) +EVENT_MAX_TOKENS=4000 + +# GPT-5 specific (only used with GPT-5 models and OpenAI API) +GPT5_REASONING_EFFORT=medium # minimal, low, medium, high +GPT5_VERBOSITY=medium # low, medium, high + +# Chat model configuration (optional - falls back to TEXT_MODEL_* if not set) +CHAT_MODEL_API_KEY=your_chat_api_key +CHAT_MODEL_BASE_URL=https://openrouter.ai/api/v1 +CHAT_MODEL_NAME=openai/gpt-4o + +# Chat-specific GPT-5 settings (optional - falls back to GPT5_* if not set) +CHAT_GPT5_REASONING_EFFORT=medium # minimal, low, medium, high +CHAT_GPT5_VERBOSITY=medium # low, medium, high +``` + +## Per-Upload, Per-Tag, Per-Folder Transcription Models + +By default Speakr uses the single `TRANSCRIPTION_MODEL` set in `.env` for every recording. If your users transcribe different kinds of recordings (calls, meetings, dictations, multi-speaker interviews) you can publish a list of models they're allowed to choose from at upload time. + +```bash +# Comma-separated list of model identifiers users can pick. +TRANSCRIPTION_MODELS_AVAILABLE=whisper-1,gpt-4o-transcribe,gpt-4o-transcribe-diarize,vibevoice +# Optional parallel list of display names. Falls back to the model id when omitted. +TRANSCRIPTION_MODEL_LABELS=Whisper,GPT-4o,GPT-4o (Diarize),VibeVoice +``` + +When the list is non-empty, a "Transcription model" dropdown appears in the upload form's Advanced ASR Options and in the reprocess modal. Tags and folders also gain a "Default transcription model" field in their edit forms — set one and any recording uploaded with that tag or in that folder uses the chosen model unless the user picks a different one at upload time. + +Resolution order at upload time: + +1. Per-upload selection (Advanced ASR Options dropdown) +2. First tag's `default_transcription_model` +3. Folder's `default_transcription_model` +4. Global `TRANSCRIPTION_MODEL` env var (current behaviour) + +If the override isn't in `TRANSCRIPTION_MODELS_AVAILABLE`, it's silently dropped and Speakr falls back to the global default — useful as a safety net against stale browser caches sending old model ids. + +The override is propagated to the connector via the `model` field on `TranscriptionRequest`. Connectors that key on a model name (OpenAI Whisper / Transcribe, Mistral, VibeVoice) honour it directly. The `asr_endpoint` connector forwards the override as a `model=` query parameter; the [whisperx-asr-service](https://github.com/murtaza-nasir/whisperx-asr-service) fork uses it to switch the loaded Whisper model on demand, while the upstream `ahmetoner/whisper-asr-webservice` ignores unknown query parameters, so the override is safe in either case. + +!!! warning "List only models compatible with the active connector" + The dropdown changes the model name **within the currently active connector**; it does not switch between providers. Speakr selects exactly one connector at startup based on `TRANSCRIPTION_CONNECTOR`, `USE_ASR_ENDPOINT`, and `TRANSCRIPTION_BASE_URL`, and every transcription request is routed there. If the list contains a model name the active connector does not recognise (for example, putting `gpt-4o-transcribe` in the list while `USE_ASR_ENDPOINT=true` is set), requests for that model fail with a 500 from the upstream service. + + For a WhisperX backend, list Whisper variants (`large-v3`, `medium`, `distil-medium.en`, etc.). For OpenAI's API, list `whisper-1`, `gpt-4o-transcribe`, `gpt-4o-mini-transcribe`, `gpt-4o-transcribe-diarize`. Mixing providers in one dropdown is a planned feature for a future release. + +### Admin-Managed Model List with `/v1/models` Discovery + +For connectors that expose an OpenAI-compatible `/v1/models` endpoint (OpenAI, Azure OpenAI, vLLM, and the WhisperX ASR service when configured to advertise its loaded models), admins can curate the available list directly from the **Default Prompts → Transcription Models** section of the admin dashboard rather than via env var. Click "Refresh from connector" to query `/v1/models`, tick the models you want users to see, and save. + +The DB-backed list (stored in the `system_setting` table) overrides `TRANSCRIPTION_MODELS_AVAILABLE` when set. To revert to env-var control, clear the list in the dashboard. The env var is still useful for installations where the connector does not advertise its models or where you want config-as-code behaviour. + +When a connector returns a richer model object (display name, description, supported languages), those fields are used to render the dropdown labels. Otherwise the model id is used as both id and label, the same as with `TRANSCRIPTION_MODEL_LABELS`. + +!!! note "Localising model labels" + Model identifiers are typically brand names (Whisper, GPT-4o, VibeVoice) and are not translated by Speakr's i18n system. If you want localised labels for users on a non-English UI, set `TRANSCRIPTION_MODEL_LABELS` to your translated names: the labels you provide are used verbatim in the dropdown regardless of UI language. The same applies to admin-managed lists curated from the dashboard. + +## Configurable Embedding Model + +Speakr's Inquire mode (semantic search) uses [sentence-transformers](https://www.sbert.net/) to embed transcript chunks locally by default. The default model is `all-MiniLM-L6-v2` (384-dim vectors), which is fast, small, and sufficient for most use cases. + +To use a different local model, set: + +```bash +EMBEDDING_MODEL=all-mpnet-base-v2 # 768-dim, higher quality +# or +EMBEDDING_MODEL=multi-qa-MiniLM-L6-cos-v1 # tuned for question-answering +``` + +Any sentence-transformers compatible model name works. + +### Remote (API-based) Embeddings + +If you would rather offload embeddings to an OpenAI-compatible HTTP endpoint (vLLM, OpenRouter, OpenAI, Together, or any other compatible provider), set `EMBEDDING_BASE_URL` and Speakr switches to API mode. The same `EMBEDDING_MODEL` env var becomes the model identifier sent in each request. + +```bash +# OpenAI directly +EMBEDDING_BASE_URL=https://api.openai.com/v1 +EMBEDDING_API_KEY=sk-... +EMBEDDING_MODEL=text-embedding-3-small +EMBEDDING_DIMENSIONS=768 # optional, only honoured by providers that support output-dim trimming +``` + +```bash +# Self-hosted vLLM serving an embedding model +EMBEDDING_BASE_URL=http://vllm-host:8000/v1 +EMBEDDING_API_KEY=not-needed +EMBEDDING_MODEL=BAAI/bge-large-en-v1.5 +``` + +```bash +# OpenRouter +EMBEDDING_BASE_URL=https://openrouter.ai/api/v1 +EMBEDDING_API_KEY=sk-or-... +EMBEDDING_MODEL=openai/text-embedding-3-large +``` + +API mode does not require sentence-transformers to be installed, so the lightweight Docker image (`learnedmachine/speakr:lite`) can now run full semantic search by combining `EMBEDDING_BASE_URL` with any compatible provider. The lite image already ships `openai` and `scikit-learn`, so no further dependencies are needed. + +### Compatibility Note + +The active embedding identifier (provider plus model) is recorded in `system_setting` on first startup. If you later change `EMBEDDING_MODEL` or `EMBEDDING_BASE_URL`, Speakr logs a warning at startup and Inquire mode will return wrong results until you reprocess affected recordings, because chunks embedded with the previous configuration occupy a different vector space. To rebuild embeddings after a change, reprocess each recording. + +## Mistral Voxtral Chunking + +Voxtral handles up to 3 hours per request natively, but the cloud API can time out near that limit on long meeting recordings. To opt into app-side chunking for the Mistral connector: + +```bash +TRANSCRIPTION_CONNECTOR=mistral +MISTRAL_ENABLE_CHUNKING=true +MISTRAL_MAX_DURATION_SECONDS=7200 # 2 hours; chunks at 80% of this +``` + +Diarization across chunks: Mistral doesn't return voice embeddings, so speakers are remapped per chunk (`SPEAKER_00` in chunk 1 ≠ `SPEAKER_00` in chunk 2). If you need consistent speaker identity across an entire long recording, use `gpt-4o-transcribe-diarize` (uses known-speaker references) or `whisperx-asr-service` with embeddings enabled. + +## Troubleshooting + +### Model Not Responding + +Check logs for authentication errors: +```bash +docker compose logs -f app | grep "LLM" +``` + +Common issues: + +- Invalid API key +- Model name not available on your plan +- Rate limits exceeded +- Insufficient credits + +### Poor Summary Quality + +Try these adjustments: + +- Upgrade to a more capable model +- Increase `SUMMARY_MAX_TOKENS` +- Review and refine [custom prompts](prompts.md) +- For GPT-5: increase reasoning effort to `medium` or `high` + +### High Costs + +Reduce costs with: + +- Switch to smaller models (`gpt-5-nano`, `gpt-4o-mini`) +- Lower token limits +- For GPT-5: reduce reasoning effort to `minimal` or `low` +- Use OpenRouter for better rates + +## Additional Resources + +- [OpenAI GPT-5 Documentation](https://platform.openai.com/docs/guides/latest-model) +- [OpenAI Chat Completions API](https://platform.openai.com/docs/api-reference/chat) +- [OpenRouter Documentation](https://openrouter.ai/docs) +- [Custom Prompts Guide](prompts.md) +- [System Settings](system-settings.md) + +--- + +Next: [Default Prompts](prompts.md) | Back to [Admin Guide](index.md) diff --git a/docs/admin-guide/prompts.md b/docs/admin-guide/prompts.md new file mode 100644 index 00000000..a8fde0e6 --- /dev/null +++ b/docs/admin-guide/prompts.md @@ -0,0 +1,163 @@ +# Default Prompts + +The Default Prompts tab lets you shape how AI interprets and [summarizes recordings](../features.md#automatic-summarization) across your entire Speakr instance. This is where you establish the baseline intelligence that users experience when they haven't customized their own [personal prompts](../user-guide/settings.md#custom-prompts-tab). + +![Default Prompts](../assets/images/screenshots/Admin default prompts.png) + +## Understanding Prompt Hierarchy + +Speakr uses a sophisticated hierarchy to determine which prompt to use for any given recording. This system provides flexibility while maintaining control, ensuring users get appropriate summaries while allowing customization where needed. + +At the top of the hierarchy are tag-specific prompts. When a recording has [tags](../user-guide/settings.md#tag-management-tab) with associated prompts, these take absolute priority. Learn about [tag management](../features.md#tagging-system) in the features guide. Multiple tag prompts concatenate intelligently, allowing sophisticated prompt stacking for specialized content types. + +Next comes the user's personal custom prompt, set in their [account settings](../user-guide/settings.md#custom-prompts-tab). Users can also configure [language preferences](../user-guide/settings.md#language-preferences) for their summaries. This allows individuals to tailor summaries to their specific needs without affecting others. Many users never set this, making your admin default even more important. + +Your admin default prompt, configured on this page, serves as the foundation for most summaries. This is what new users experience and what long-term users rely on when they haven't customized their settings. It shapes the overall intelligence and utility of your Speakr instance. + +Finally, if all else fails, a hardcoded system fallback ensures summaries are always generated. You'll rarely see this in practice, but it provides a safety net ensuring the system never fails to produce output. + +## Crafting Effective Default Prompts + +Your default prompt is more than technical instruction - it's a template for understanding. The prompt shown in the interface demonstrates a balanced approach, requesting key issues, decisions, and action items. This structure works well for business meetings but might not suit all contexts. + +Consider your user base when designing prompts. A research institution might emphasize methodologies and findings. A legal firm could focus on case details and precedents. For multi-language support, see [language configuration](../features.md#language-support) and [troubleshooting language issues](../troubleshooting.md#summary-language-doesnt-match-preference). A creative agency might highlight concepts and client feedback. The prompt should reflect what matters most to your users. + +The AI responds best to clear, structured requests. Use bullet points or numbered sections to organize the output. Specify the level of detail you want - "brief overview" versus "comprehensive analysis" produces very different results. Include examples if certain formats are crucial. + +Remember that this prompt applies to everything from five-minute check-ins to two-hour workshops. Design for versatility. Avoid overly specific requirements that might not apply to all content. Focus on extracting universally valuable information while allowing the AI flexibility to adapt to different recording types. + +## The Default Prompt Editor + +The large text area displays your current default prompt, with full markdown support for formatting. You can use bold for emphasis, lists for structure, and even code blocks if you need to show example formats. The editor expands to accommodate longer prompts, though conciseness generally produces better results. + +Changes save immediately when you click the Save Changes button. There's no draft or staging - modifications affect all new summaries instantly. Users can [regenerate summaries](../user-guide/transcripts.md) to apply updated prompts to existing recordings. The Reset to Default button provides a safety net, reverting to the original prompt if your customizations don't work as expected. + +The timestamp shows when the prompt was last modified, helpful for tracking changes over time. If multiple admins manage your instance, this helps coordinate who changed what and when. + +## Understanding the LLM Prompt Structure + +The expandable "View Full LLM Prompt Structure" section reveals how your prompt fits into the complete instruction sent to the AI. This technical view shows the system prompt, your custom prompt, and the transcript integration. + +Understanding this structure helps you write better prompts. You'll see that certain instructions are already handled by the system prompt, so you don't need to repeat them. You'll understand how your prompt interacts with the transcript text and why certain phrasings work better than others. + +This transparency also helps with troubleshooting. If summaries aren't meeting expectations, reviewing the full prompt structure often reveals why. Perhaps your instructions conflict with system instructions, or maybe you're requesting information that isn't typically in transcripts. + +## Practical Prompt Strategies + +Start with a proven structure and iterate based on results. The default prompt works well because it requests concrete, actionable information. Key issues provide context, decisions document outcomes, and action items drive follow-up. + +Test your prompts with various recording types before deploying widely. A prompt that works beautifully for formal presentations might fail for casual conversations. Upload test recordings with different characteristics and evaluate the summaries produced. + +Consider seasonal or project-based adjustments. During planning seasons, you might emphasize goals and strategies. During execution phases, focus on progress and blockers. You can update the default prompt as organizational needs evolve. + +Monitor user feedback about summary quality. If users frequently edit summaries or complain about missing information, your prompt might need adjustment. The best prompt is one users rarely need to modify. + +## Advanced Prompt Techniques + +Layer instructions for nuanced output. Instead of just requesting "action items," specify "action items with responsible parties and due dates if mentioned." This precision helps the AI extract more valuable information when it's available. + +Use conditional language for flexibility. Phrases like "if applicable" or "when discussed" allow the AI to skip sections that don't apply to every recording. This prevents forced, irrelevant content in summaries. + +Consider the AI model's strengths and limitations. Current models excel at identifying themes, extracting specific information, and organizing content. They struggle with complex reasoning, mathematical calculations, and information not explicitly stated. Design prompts that play to these strengths. + +Balance detail with readability. Extremely detailed prompts might produce comprehensive summaries that users don't read. Sometimes a concise, focused summary serves users better than exhaustive documentation. + +## Creative Tag Prompt Use Cases + +Tags with custom prompts unlock powerful transformation capabilities. Here are creative ways people use this feature: + +### Recipe Recordings + +Create a "Recipe" tag with a prompt like: "Convert this free-form cooking narration into a properly formatted recipe with ingredients list, step-by-step instructions, cooking times, and servings. Organize ingredients by quantity and item. Number the steps clearly." + +When you record yourself cooking while talking through what you're doing - "okay I'm adding maybe two cups of flour, bit more actually, and then half a cup of sugar, no wait three quarters" - the AI transforms that messy stream-of-consciousness into a clean, usable recipe format with organized ingredients and numbered steps. + +### Lecture Notes + +A "Lecture" tag could use: "Extract the main concepts, supporting examples, key terminology with definitions, and any practical applications mentioned. Organize in an outline format suitable for study notes." + +Students record lectures as they happen. The messy 90-minute recording becomes organized study notes with concepts clearly labeled, examples pulled out, and terminology defined. Much more useful than trying to review the raw transcript. + +### Meeting Action Items + +Create a "Project Meeting" tag with: "Focus exclusively on action items, decisions made, and next steps. For each action item, identify who is responsible and any mentioned deadlines. Ignore general discussion." + +The group spends an hour talking about a project. The summary ignores all the background discussion and debate, giving you just the concrete outcomes - who's doing what and when. + +### Brainstorming Sessions + +A "Ideas" tag with: "List every distinct idea mentioned, no matter how brief the discussion. For each idea, note any immediate reactions or concerns raised. Don't evaluate or synthesize - just capture everything." + +Free-flowing creative sessions produce transcripts full of half-formed thoughts and tangents. This prompt pulls out every idea fragment so nothing gets lost in the noise. + +### Code Review Sessions + +"Code Review" tag: "For each piece of code or system discussed, list: 1) What was reviewed, 2) Issues identified, 3) Suggested changes, 4) Who will implement fixes. Use technical language, don't simplify." + +Technical discussions stay technical. The summary uses proper terminology and maintains the level of detail needed for developers to act on the feedback. + +## Tag Stacking and Order + +When a recording has multiple tags with prompts, they concatenate in the order tags were applied. This creates powerful combinations: + +### Example: Personal Lecture + Specific Course + +You have a personal "My Lectures" tag with: "Organize as study notes with clear headers." + +You also tag with "Biology 301" which adds: "Pay special attention to biological processes, terminology, and diagrams mentioned." + +The result combines both: study notes format focused on biological content. The order doesn't matter much here since they're complementary. + +### Example: Client Meeting + Legal Review + +"Client Meeting" tag: "Extract client requirements, concerns, and preferences." + +"Legal Review" tag: "Identify any legal considerations, compliance requirements, or risk factors mentioned." + +Together, you get client needs plus legal implications in one summary - useful when client calls touch on contractual matters. If you tagged "Legal Review" first and "Client Meeting" second, the legal aspects would be emphasized first, then client concerns. + +### Example: Recipe + Dietary Restriction + +"Recipe" tag: "Convert to formatted recipe." + +"Gluten Free" tag: "Note which ingredients contain gluten and suggest substitutions." + +The recipe gets formatted properly, plus you get automatic gluten-free adaptation notes. Perfect when you're adapting traditional recipes for dietary needs. + +### When Order Matters + +More specific prompts should generally come last, as they refine the output from general prompts. Start broad (format type) then add specifics (focus areas). + +If you tag "Technical Details" + "Executive Summary", you're asking for detailed technical content presented as an executive summary - probably condensed but still technical. + +Reverse it to "Executive Summary" + "Technical Details" and you're requesting executive-level content with technical depth where applicable - probably less detailed overall. + +Test your tag combinations with sample recordings to see which order produces the results you want. + +## Coordinating with User Prompts + +Your default prompt should complement, not compete with, user customization. Design it as a solid foundation that works for most cases while encouraging power users to customize for their specific needs. + +Communicate your prompt strategy to users. Let them know what the default prompt emphasizes so they can decide whether customization would benefit them. Share examples of effective user prompts that build on your default. + +Consider documenting prompt best practices for your users. If certain departments need specialized summaries, provide recommended prompts they can use. This empowers users while maintaining consistency where it matters. + +## Measuring Prompt Effectiveness + +Track how often users modify AI-generated summaries. Frequent edits suggest your prompt isn't capturing what users need. Minimal edits indicate your prompt effectively extracts valuable information. + +Review a sample of summaries periodically. Do they consistently include the requested sections? Is the information accurate and relevant? Are users adding similar information that the prompt should request? + +Gather feedback during user reviews or support interactions. Ask specifically about summary quality and whether the default format meets their needs. Users who don't customize their prompts rely entirely on your default, making their feedback crucial. + +## Common Prompt Pitfalls + +Avoid overly restrictive prompts that force structure onto incompatible content. Not every recording has "decisions" or "action items." Forcing the AI to find these when they don't exist produces meaningless filler. + +Don't request information the AI can't provide. Asking for "unspoken concerns" or "what wasn't discussed" goes beyond transcript analysis. The AI can only work with what was actually said and recorded. + +Resist the temptation to make prompts too long. Each instruction adds complexity and potential confusion. Focus on what's most important rather than trying to capture every possible detail. + +--- + +Next: [Vector Store](vector-store.md) → \ No newline at end of file diff --git a/docs/admin-guide/retention.md b/docs/admin-guide/retention.md new file mode 100644 index 00000000..ddf7116c --- /dev/null +++ b/docs/admin-guide/retention.md @@ -0,0 +1,461 @@ +--- +layout: default +title: Retention & Auto-Deletion +parent: Admin Guide +nav_order: 7 +--- + +# Auto-Deletion and Retention Policies + +This document describes the automated retention and deletion system for Speakr recordings. + +## Overview + +The auto-deletion system provides automated lifecycle management for your recordings, helping you: + +- **Comply with data retention policies** - Automatically remove recordings after a specified retention period +- **Manage storage** - Prevent unlimited growth of audio files +- **Maintain critical data** - Keep transcriptions and metadata even after audio deletion +- **Protect important recordings** - Exempt specific recordings from automatic deletion + +## Configuration + +### Environment Variables + +Add these to your `.env` file to configure auto-deletion: + +```bash +# Enable or disable the auto-deletion feature +ENABLE_AUTO_DELETION=false # Set to 'true' to enable + +# Global retention period in days (0 = disabled) +GLOBAL_RETENTION_DAYS=90 # Recordings older than this will be processed + +# Deletion mode: what to delete +DELETION_MODE=full_recording # Options: 'audio_only' or 'full_recording' +``` + +### Deletion Modes + +#### Audio-Only Mode (`DELETION_MODE=audio_only`) +- **Deletes**: Audio file only +- **Keeps**: Transcription, summary, notes, metadata +- **Use case**: Long-term record keeping with storage optimization +- **Result**: Recordings appear in "Archived" view, transcription remains searchable + +#### Full Recording Mode (`DELETION_MODE=full_recording`) +- **Deletes**: Complete recording including audio, transcription, summary, notes +- **Keeps**: Nothing - recording is permanently removed +- **Use case**: Complete data removal for compliance +- **Result**: Recording is completely removed from the system + +## Multi-Tier Retention System + +Speakr uses a hierarchical retention policy system: + +### 1. Global Retention (System-Wide) +Set via `GLOBAL_RETENTION_DAYS` environment variable. Applies to all recordings unless overridden. + +```bash +GLOBAL_RETENTION_DAYS=90 # All recordings older than 90 days +``` + +### 2. Tag-Based Retention +Tags can override the global retention period with custom retention periods. This is especially powerful with group tags, where group admins can set retention policies for their group's content. + +``` +Global: 90 days +Tag "Legal Records": 2555 days (7 years) # Longer retention +Tag "Daily Standups": 14 days # Shorter retention +Untagged recordings: Uses global (90 days) +``` + +When a recording has multiple tags with different retention periods, the **shortest** period applies. + +### 3. Tag-Based Protection +Individual tags can protect recordings from auto-deletion entirely. + +**Example Hierarchy:** + +- Global retention: 90 days +- Tag "Sprint Reviews": 180 days (longer than global) +- Tag "Daily Standups": 14 days (shorter than global) +- Tag "Legal" with protection enabled: Never deleted (permanent) + +## Protecting Recordings from Deletion + +1. Go to **Account Settings** → **Tags** tab +2. Click **Create Tag** or **Edit** an existing tag +3. Enable **"Protect from Auto-Deletion"** checkbox +4. Apply this tag to recordings you want to protect + +**When protected:** + +- ✅ Recordings with protected tags are exempt from auto-deletion +- ✅ Works regardless of age or retention period +- ✅ Applies to all recordings with that tag + +## Archived Recordings + +When `DELETION_MODE=audio_only`, recordings become "archived" after audio deletion. + +### Accessing Archived Recordings + +1. Open the **Recordings** sidebar +2. Click **Advanced Filters** +3. Toggle **"Archived Recordings"** ON + +### What You Can Do with Archived Recordings + +| Feature | Available | Notes | +|---------|-----------|-------| +| View transcription | ✅ | Full transcript accessible | +| Search content | ✅ | Text search still works | +| Read summary | ✅ | AI summary preserved | +| View/edit notes | ✅ | All metadata accessible | +| Play audio | ❌ | Audio file deleted | +| Re-process | ❌ | Source audio unavailable | +| Share | ✅ | Can share transcription | +| Export | ✅ | Download transcript, summary, notes | + +### Archived Recording Indicators + +- **Sidebar**: Gray "Archived" badge next to recording title +- **Player**: Info banner: "Audio file has been deleted, but the transcription remains available" +- **Filter**: Separate "Archived" view toggle in advanced filters + +## Admin Controls + +### Running Auto-Deletion + +Auto-deletion runs automatically based on configured schedule. Admins can also trigger manually: + +**API Endpoint:** +```bash +POST /admin/auto-deletion/run +``` + +**Response:** +```json +{ + "checked": 150, + "deleted_audio_only": 45, + "deleted_full": 0, + "exempted": 12, + "errors": 0 +} +``` + +### Checking Auto-Deletion Stats + +**API Endpoint:** +```bash +GET /admin/auto-deletion/stats +``` + +**Response:** +```json +{ + "enabled": true, + "global_retention_days": 90, + "deletion_mode": "audio_only", + "eligible_count": 45, + "exempted_count": 12, + "archived_count": 128 +} +``` + +## Speaker Data Cleanup + +By default, speaker profiles and voice embeddings are preserved even when all associated recordings are deleted. This is because voice embeddings are aggregated values that cannot be reconstructed from individual recordings. + +To enable automatic cleanup of orphaned speaker profiles, set `DELETE_ORPHANED_SPEAKERS=true` in your environment. + +### What Gets Cleaned Up + +When `DELETE_ORPHANED_SPEAKERS=true` and the auto-deletion job runs, the system: + +1. **Removes orphaned speaker profiles**: Speakers with no remaining recordings are deleted +2. **Cleans embedding references**: Recording IDs are removed from speaker voice profile metadata +3. **Applies to both deletion modes**: Works with both `audio_only` and `full_recording` deletion modes + +### Cleanup Schedule + +Speaker cleanup runs on the same schedule as auto-deletion: + +- **Frequency**: Daily at 2:00 AM (server time) +- **Trigger**: Automatically when `ENABLE_AUTO_DELETION=true` and `DELETE_ORPHANED_SPEAKERS=true` + +### When Speakers Are Deleted + +A speaker is considered "orphaned" and deleted when: + +- `DELETE_ORPHANED_SPEAKERS=true` is set +- No `SpeakerSnippet` records exist for the speaker (no voice samples in any recordings) +- No valid recording references remain in the speaker's voice profile metadata + +**Note**: Speakers are preserved as long as they have at least one active recording with speaker identifications. + +### Privacy & GDPR Compliance + +For deployments that need to treat voice embeddings as biometric data, enable `DELETE_ORPHANED_SPEAKERS=true` to ensure: + +- **Data Minimization**: Removes voice data when no longer needed +- **Right to Erasure**: Deletes voice profiles when recordings are removed +- **Transparency**: Cleanup activity is logged for audit purposes +- **Automatic**: No manual intervention required when combined with retention policies + +### Monitoring Cleanup Activity + +View cleanup statistics in: + +- **System logs**: Check application logs for cleanup counts and activity +- **Auto-deletion response**: Speaker cleanup counts included in scheduled job output + +Example log entry: +``` +INFO - Speaker cleanup completed: 5 speakers deleted, 12 embedding references removed +``` + +The cleanup process includes these statistics in the auto-deletion job response: + +```json +{ + "checked": 123, + "deleted_audio_only": 45, + "deleted_full": 0, + "exempted": 12, + "speakers_deleted": 5, + "embeddings_cleaned": 12, + "speakers_evaluated": 94 +} +``` + +### What Data Is Retained + +The system preserves: + +- **Active speakers**: Speakers with at least one recording containing their voice +- **Speaker names**: Names are retained as long as associated recordings exist +- **Voice profiles**: Embedding data is kept when recordings reference the speaker + +### What Data Is Removed + +The system removes: + +- **Orphaned voice embeddings**: Biometric voice data for speakers with no recordings +- **Speaker records**: Entire speaker profile when completely orphaned +- **Invalid references**: Recording IDs in embedding history that point to deleted recordings +- **Usage statistics**: Use counts and timestamps for deleted speakers + +This ensures that biometric data is only retained when there's a legitimate purpose (active recordings), fulfilling GDPR's data minimization requirement. + +## Practical Use Cases + +The retention system solves real problems people have with accumulating recordings. Here's how it gets used: + +### Personal Use + +You record everything during your workday to capture ideas and discussions. Most of these recordings are ephemeral - useful for a week or two, then forgotten. Set a 30-day global retention with audio-only deletion. After a month, the audio files disappear but the searchable transcriptions remain. You can still find what was said in old recordings, but you're not paying to store hours of audio you'll never listen to again. + +If something turns out to be important, tag it with a protected tag before the 30 days expire. The tag prevents deletion, preserving both audio and transcript for as long as you need. + +### Group Collaboration + +Different types of group content need different lifecycles: + +| Content Type | Retention Approach | Why | +|--------------|-------------------|-----| +| Daily standups | Group tag with 14-day retention | Routine updates, no long-term value | +| Sprint planning | Group tag with 90-day retention | Reference value for current quarter | +| Architecture decisions | Group tag with protection enabled | Document important choices permanently | +| Customer calls (sales) | Group tag with 1-year retention | Sales cycle duration + follow-up window | +| Interviews (HR) | Group tag with 2-year retention | Typical employment litigation window | +| Legal meetings | Protected tag | Indefinite retention for compliance | + +Each group sets up their tags once with appropriate retention. Members just tag recordings normally, and lifecycle management happens automatically. Nobody has to remember which recordings to keep or delete. + +### Compliance Requirements + +Organizations with data retention policies can enforce them automatically. Healthcare organization needs 7-year retention for patient consultations - set that in the relevant group tag. Law firm needs indefinite retention for client meetings - use protected tags. Financial services deletes routine internal calls after 90 days but keeps compliance-related recordings for 7 years - different tags with different retention. + +The system enforces policy without requiring anyone to remember the rules. Tag correctly, and retention happens automatically. + +### Storage Cost Management + +Audio files are large - a one-hour meeting might be 50-100MB. Transcriptions are text - the same meeting might be 10-20KB. Audio-only deletion mode keeps the valuable searchable text while reclaiming storage. + +Run audio-only deletion with a 90-day retention. Recordings older than 90 days lose their audio but remain fully searchable. You can still use Inquire Mode to find information, read transcripts, review summaries, and see notes. You just can't play the original audio. For most use cases, that's fine - once you've extracted the information into text, the audio serves no purpose. + +This approach lets you keep years of searchable conversation history without accumulating terabytes of audio files. + +## Best Practices + +### For Compliance + +1. **Set appropriate retention periods** + ```bash + # Example: 7-year retention for financial records + GLOBAL_RETENTION_DAYS=2555 # 7 years × 365 days + DELETION_MODE=full_recording + ``` + +2. **Use tag-based protection** for records requiring indefinite retention + - Create "Legal Hold" or "Permanent" tags + - Enable protection on these tags + - Apply to relevant recordings + +3. **Document your retention policy** in your organization's compliance documentation + +### For Storage Management + +1. **Start with audio-only deletion** + ```bash + DELETION_MODE=audio_only + ``` + - Keeps searchable transcriptions + - Frees up 95%+ of storage (audio files are large) + - Maintains business value of conversations + +2. **Use shorter retention periods** for routine recordings + ```bash + GLOBAL_RETENTION_DAYS=30 # Routine meetings + ``` + +3. **Protect important content** with tags + - "Executive Meetings" tag → protect from deletion + - "Daily Standup" tag → no protection (routine) + +### For Groups + +When groups are enabled: + +1. **Set conservative global retention** (shorter period as a baseline) +2. **Configure group tags with custom retention** to match each group's needs +3. **Use protected group tags** for group content requiring permanent retention +4. **Document retention policies** so group members understand lifecycle expectations + +Example group tag retention configuration: + +- Engineering group "Architecture Decisions": Protected (never deleted) +- Sales group "Customer Calls": 365 days +- HR group "Interviews": 90 days +- Operations group "Daily Standups": 14 days + +## Deletion Process Flow + +``` +1. Automated Check (Daily/Manual Trigger) + ↓ +2. Find recordings older than retention period + ↓ +3. For each recording: + - Check manual exemption flag + - Check tags for protection + - Skip if exempt + ↓ +4. Delete based on mode: + - audio_only: Remove file, keep DB record, set audio_deleted_at + - full_recording: Remove file and DB record + ↓ +5. Return statistics +``` + +## Migration Guide + +### Enabling Auto-Deletion on Existing System + +1. **Test with audio-only mode first:** + ```bash + ENABLE_AUTO_DELETION=true + GLOBAL_RETENTION_DAYS=365 # Start with long period + DELETION_MODE=audio_only # Test safely + ``` + +2. **Protect existing important content:** + - Create protected tags + - Apply to critical recordings + - Verify exemptions via `/admin/auto-deletion/stats` + +3. **Run manual test:** + ```bash + POST /admin/auto-deletion/run + ``` + +4. **Monitor results** and adjust retention period as needed + +### Reverting Changes + +If you need to disable auto-deletion: +```bash +ENABLE_AUTO_DELETION=false +``` + +**Note:** Already deleted audio files cannot be recovered. Database records (if using audio-only mode) remain intact. + +## API Reference + +### Run Auto-Deletion (Admin Only) + +```http +POST /admin/auto-deletion/run +``` + +Manually trigger the auto-deletion process. + +### Get Deletion Statistics (Admin Only) + +```http +GET /admin/auto-deletion/stats +``` + +Get statistics about eligible recordings and current configuration. + +## Troubleshooting + +### Auto-Deletion Not Running + +**Check:** + +1. `ENABLE_AUTO_DELETION=true` in `.env` +2. `GLOBAL_RETENTION_DAYS > 0` +3. Admin status for manual triggers +4. Server logs for errors + +### Too Many Recordings Being Deleted + +**Solutions:** + +1. Increase `GLOBAL_RETENTION_DAYS` +2. Add protected tags to important categories +3. Check tag assignments on recordings +4. Review exemption status via stats endpoint + +### Archived Recordings Not Showing + +**Check:** + +1. Toggle "Archived Recordings" filter in sidebar +2. Verify `DELETION_MODE=audio_only` (full_recording doesn't archive) +3. Check `audio_deleted_at` field in database + +## Security Considerations + +1. **Admin-only endpoints**: Auto-deletion triggers require admin authentication +2. **Irreversible deletion**: Deleted audio files cannot be recovered +3. **Audit trail**: Check server logs for deletion events +4. **GDPR compliance**: Full deletion mode helps meet "right to be forgotten" requirements + +## Support + +For issues or questions about auto-deletion: + +1. Check server logs for detailed error messages +2. Verify environment variable configuration +3. Test with `/admin/auto-deletion/stats` endpoint +4. Review this documentation +5. Submit issues on GitHub with logs attached + +--- + +Return to [Admin Guide](index.md) → diff --git a/docs/admin-guide/sso-setup.md b/docs/admin-guide/sso-setup.md new file mode 100644 index 00000000..d213fe49 --- /dev/null +++ b/docs/admin-guide/sso-setup.md @@ -0,0 +1,124 @@ +# SSO Setup (OIDC) + +This guide explains how to enable Single Sign-On (SSO) for Speakr using any OpenID Connect (OIDC) identity provider such as Keycloak, Azure AD/Entra ID, Google, or Auth0. + +## Prerequisites + +- Speakr server reachable by the IdP at the redirect URL you configure. +- Client ID and Client Secret issued by your IdP. +- OIDC discovery (well-known) URL from your IdP. + +## Required environment variables + +Set these variables (see `config/env.sso.example`): + +``` +ENABLE_SSO=true +SSO_PROVIDER_NAME=Keycloak +SSO_CLIENT_ID=speakr +SSO_CLIENT_SECRET=change-me +SSO_DISCOVERY_URL=https://keycloak.example.com/realms/master/.well-known/openid-configuration +SSO_REDIRECT_URI=https://speakr.example.com/auth/sso/callback + +# Auto-registration (email domain filter) +SSO_AUTO_REGISTER=true +SSO_ALLOWED_DOMAINS=example.com,company.org + +# Disable password login for regular users (optional) +SSO_DISABLE_PASSWORD_LOGIN=false + +# Claim mapping (optional) +SSO_DEFAULT_USERNAME_CLAIM=preferred_username +SSO_DEFAULT_NAME_CLAIM=name +``` + +Restart Speakr after updating environment variables. + +## Claim expectations + +- `sub` (required): stable subject identifier. +- `email` (recommended): used for matching and domain allowlist. +- `preferred_username` or `name`: used for username/full name if provided. + +## Keycloak quick start + +1. In Keycloak, create a new client (e.g., `speakr`) with: + - **Client Type**: OpenID Connect + - **Access Type**: Confidential + - **Valid Redirect URI**: `https://your-host/auth/sso/callback` + - **Web Origins**: `+` (or your domain) +2. Copy the **Client ID** and **Client Secret**. +3. Note the **OpenID Endpoint Configuration** (discovery) URL, typically: + `https:///realms//.well-known/openid-configuration` + +4. Set the environment variables accordingly and restart Speakr. + +## Azure AD / Entra ID quick start + +1. Create an App Registration. +2. Add a **Web Redirect URI**: `https://your-host/auth/sso/callback`. +3. Grant API permissions: `openid`, `profile`, `email`. +4. Create a client secret. +5. Discovery URL format: + `https://login.microsoftonline.com//v2.0/.well-known/openid-configuration` + +6. Set variables and restart. + +## Google quick start + +1. Create OAuth credentials (Web application). +2. Add authorized redirect URI: `https://your-host/auth/sso/callback`. +3. Use discovery URL: + `https://accounts.google.com/.well-known/openid-configuration` + +4. Set variables and restart. + +## Auth0 quick start + +1. Create a Regular Web Application. +2. Allowed Callback URLs: `https://your-host/auth/sso/callback`. +3. Discovery URL: + `https://.auth0.com/.well-known/openid-configuration` + +4. Set variables and restart. + +## Auto-registration behavior + +- If `SSO_AUTO_REGISTER=true`, new users are created on first login when their email domain is allowed (or when allowlist is empty). +- If `SSO_AUTO_REGISTER=false`, only existing users with a linked SSO subject can sign in. +- Email domain allowlist is enforced only when an email is present. + +## Disabling password login + +Set `SSO_DISABLE_PASSWORD_LOGIN=true` to enforce SSO-only authentication for regular users. When enabled: + +- The login page shows only the SSO sign-in button +- Regular users cannot log in with email/password +- **Administrators can still use password login** as a fallback (hidden behind "Administrator login" link) + +This is useful for organizations that want to enforce SSO for all users while keeping emergency admin access available. + +## Security note + +When a user logs in via SSO with an email that matches an existing Speakr account, the accounts are automatically linked. This is convenient for most setups but relies on trusting your IdP to provide accurate email information. + +For self-hosted deployments where you control both Speakr and the IdP, this is generally not a concern. If you're using an IdP where users can set unverified email addresses, be aware that this could allow account linking without email ownership verification. Consider using `SSO_ALLOWED_DOMAINS` to restrict which email domains can authenticate. + +## Linking existing users + +- In **Account > Single Sign-On**, click **Link {PROVIDER} account** while logged in. +- If the SSO subject is already linked to another user, the link is rejected. + +## Unlinking SSO + +Users can unlink their SSO account from **Account > Single Sign-On** by clicking **Unlink {PROVIDER} account**. This removes the SSO association while keeping the local account intact. + +**Important:** Users who created their account via SSO (and have no password set) must first set a password before unlinking. Otherwise they would be locked out of their account. + +## Troubleshooting + +- **Login fails immediately**: verify `SSO_DISCOVERY_URL`, client credentials, and that the redirect URI matches exactly. +- **User created without email**: some IdPs do not return `email`; user is created with a placeholder email based on `sub`. +- **Domain rejected**: confirm `SSO_ALLOWED_DOMAINS` and that the IdP returns an `email` claim. +- **Already linked**: ensure each SSO subject is unique; users can unlink from Account settings to re-link to a different account. + diff --git a/docs/admin-guide/statistics.md b/docs/admin-guide/statistics.md new file mode 100644 index 00000000..4944f8af --- /dev/null +++ b/docs/admin-guide/statistics.md @@ -0,0 +1,83 @@ +# System Statistics + +The System Statistics tab transforms raw data into actionable insights about your Speakr instance. At a glance, you can see how many users you're serving, how many recordings they've created, how much storage they're consuming, and whether everything is processing smoothly. + +![System Statistics](../assets/images/screenshots/Admin stats.png) + +## Key Metrics Overview + +Four prominent cards at the top of the statistics page give you immediate insight into your system's scale. Total Users shows your current user base size, helping you understand your instance's reach. Total Recordings reveals the cumulative content in your system, while Total Storage presents the actual disk space consumed. Total Queries, when Inquire Mode is enabled, indicates how actively users are searching their recordings. + +These numbers tell a story about your instance's health and growth. A growing user count with proportional recording growth suggests healthy adoption. Storage growing faster than recordings might indicate users are uploading longer files. Query counts reveal whether users are finding value in the semantic search features. + +## Recording Status Distribution + +The status distribution section breaks down your recordings into four critical states. Completed recordings are fully processed and ready for use - this should be the vast majority of your content. Processing recordings are currently being transcribed or analyzed. Pending recordings are queued and waiting their turn. Failed recordings encountered errors and need attention. + +In a healthy system, you'll see mostly completed recordings with perhaps a few processing at any given moment. A large number of pending recordings might indicate your system is overwhelmed or that background processing has stopped. Failed recordings always deserve investigation - they might reveal configuration issues, API problems, or corrupted files that users are trying to upload. + +## Storage Analysis + +The "Top Users by Storage" section reveals who's consuming the most resources in your system. Each user is listed with their total storage consumption and recording count, giving you context about whether they have many small files or fewer large ones. + +This information proves invaluable for capacity planning and user education. If one user consumes disproportionate storage, you might need to understand their use case better. Are they recording multi-hour meetings? Keeping everything forever? Understanding the why behind the numbers helps you make better policy decisions. + +## Understanding Usage Patterns + +Statistics aren't just numbers - they're insights waiting to be discovered. Sudden spikes in recordings might coincide with project kickoffs, academic semesters, or company initiatives. Storage growth that outpaces recording growth could indicate users are uploading longer content or higher quality audio files. + +Regular monitoring helps you spot trends before they become problems. If storage grows 10% monthly, you can project when you'll need to expand capacity. If failed recordings suddenly spike, you can investigate whether an API key expired or a service is down. + +## Capacity Planning + +System statistics are your crystal ball for infrastructure needs. Storage growth trends tell you when you'll need more disk space. User growth patterns indicate when you might need to scale your server resources. Processing queues reveal whether your current setup can handle the workload. + +Use these insights proactively. If you see storage growing at 50GB monthly and you have 200GB free, you know you have about four months before needing intervention. This lead time lets you budget for upgrades, plan migrations, or implement retention policies before hitting critical limits. + +## Token Usage Statistics + +The Token Usage section provides visibility into LLM API consumption across your instance. Two cards split usage between **LLM operations** (title generation, summarization, chat, event extraction) and **embeddings** (Inquire mode), since they typically come from different providers and the embedding cost is otherwise easy to miss. + +**Per-Operation Breakdown**: LLM token usage is broken down by operation type so you can see where the spend is going. Title generation, summarization, chat, and event extraction each get their own line with input tokens, output tokens, and estimated cost. Embedding usage is shown as a separate card with its own daily/monthly chart, so a model swap or a re-embed-all run is visible without polluting the LLM-side numbers. This separation matters because the embedding API price (per million tokens) is usually orders of magnitude lower than chat-completion pricing, and mixing them makes both numbers harder to read. + +**Daily and Monthly Charts**: Interactive charts display token consumption trends over the last 30 days and 12 months for both LLM and embedding usage. These visualisations help identify usage patterns and predict future costs. + +**Per-User Breakdown**: A detailed table shows each user's monthly token consumption alongside their budget limit (if set). Progress bars indicate how much of their budget has been used: + +- Green: Under 80% of budget +- Yellow: Between 80-100% (warning zone) +- Red: At or over 100% (blocked) + +**Cost Tracking**: When using OpenRouter or other providers that return cost information, the statistics include estimated costs based on actual API responses. For embeddings, the cost is calculated from the configured per-million-token price for the active embedding provider. This helps with budgeting and identifying high-cost operations. + +Use token statistics to identify heavy users, validate budget allocations, and forecast API costs. If certain users consistently hit their limits, you may need to increase their budgets or investigate their usage patterns. See [Token Budget Management](user-management.md#token-budget-management) for setting individual user limits. + +## Transcription Usage Statistics + +The Transcription Usage section provides visibility into speech-to-text API consumption across your instance. This is separate from token usage and tracks audio transcription specifically. + +**Summary Cards**: Four cards at the top show: + +- **Today's Minutes**: Transcription minutes used today across all users +- **This Month**: Total minutes transcribed in the current calendar month +- **Monthly Cost**: Estimated costs based on connector pricing (OpenAI Whisper/Transcribe charges, $0 for self-hosted ASR) +- **Budget Warnings**: Count of users approaching (80%+) or exceeding (100%) their transcription budgets + +**Per-User Breakdown**: A detailed list shows each user's monthly transcription usage alongside their budget limit (if set). Progress bars indicate budget consumption: + +- Green: Under 80% of budget +- Yellow: Between 80-100% (warning zone) +- Red: At or over 100% (blocked from new transcriptions) + +**Cost Estimation**: The system calculates estimated costs based on the transcription connector used: + +- OpenAI Whisper API: $0.006 per minute +- OpenAI Transcribe (gpt-4o-transcribe): $0.006 per minute +- OpenAI Transcribe (gpt-4o-mini-transcribe): $0.003 per minute +- Self-hosted ASR endpoints: $0 (no external API costs) + +Use transcription statistics to monitor usage patterns, identify heavy users, and validate budget allocations. Organizations using cloud transcription services can forecast costs accurately, while those with self-hosted ASR can track capacity utilization. See [Transcription Budget Management](user-management.md#transcription-budget-management) for setting individual user limits. + +--- + +Next: [System Settings](system-settings.md) → \ No newline at end of file diff --git a/docs/admin-guide/system-settings.md b/docs/admin-guide/system-settings.md new file mode 100644 index 00000000..a18d8616 --- /dev/null +++ b/docs/admin-guide/system-settings.md @@ -0,0 +1,248 @@ +# System Settings + +System Settings is where you configure the fundamental behaviors that affect every user and recording in your Speakr instance. These global parameters shape how the system operates, from technical limits to user-facing features. + +![System Settings](../assets/images/screenshots/Admin system settings.png) + +## Transcript Length Limit + +The transcript length limit determines how much text gets sent to the AI when generating summaries or responding to chats. This seemingly simple number has a big effect on both quality and cost. + +When set to "No Limit," the entire transcript goes to the AI regardless of length. This ensures the AI has complete context but can become expensive for long recordings. A two-hour meeting might generate 20,000 words of transcript, consuming significant API tokens and potentially overwhelming the AI model's context window. This limit will also be applied to the speaker auto-detection feature in the speaker identification modal. + +Setting a character limit (like 50,000 characters) creates a ceiling on API consumption. The system will truncate very long transcripts, sending only the beginning portion to the AI. This keeps costs predictable but might mean the AI misses important content from later in the recording. + +The sweet spot depends on your use case. For typical meetings under an hour, 50,000 characters usually captures everything. For longer sessions, you might increase this limit or train users to split recordings. Monitor your API costs and user feedback to find the right balance. + +## Maximum File Size + +The file size limit protects your system from being overwhelmed by massive uploads while ensuring users can work with reasonable recordings. The default 300MB accommodates several hours of compressed audio, which covers most use cases. + +Raising this limit allows longer recordings but requires careful consideration. Larger files take longer to upload, consume more storage, and might timeout during processing. Your server needs enough memory to handle these files, and your storage must accommodate them. Network timeouts, browser limitations, and user patience all factor into what's practical. + +If users frequently hit the limit, consider whether they really need single recordings that long. Often, splitting long sessions into logical segments produces better results - easier to review, faster to process, and more focused summaries. + +## ASR Timeout Settings + +The ASR timeout determines how long Speakr will wait for advanced transcription services to complete their work. The default 1,800 seconds (30 minutes) handles most recordings, but you might need to adjust based on your transcription service and typical file sizes. + +Setting this too low causes longer recordings to fail even when the transcription service is working normally. The recording appears stuck in processing, then eventually fails, frustrating users who must retry or give up. Setting it too high ties up system resources waiting for services that might have actually failed. + +Your optimal timeout depends on your transcription service's performance and your users' recording lengths. Monitor processing times for successful transcriptions and set the timeout comfortably above your longest normal processing time. If you regularly process multi-hour recordings, you might need 3,600 seconds or more. + +## Recording Disclaimer + +The recording disclaimer appears before users start any recording session, making it perfect for legal notices, policy reminders, or usage guidelines. This markdown-formatted message ensures users understand their responsibilities before creating content. + +Organizations often use this for compliance requirements - reminding users about consent requirements, data handling policies, or appropriate use guidelines. Educational institutions might note that recordings are for academic purposes only. Healthcare organizations could reference HIPAA compliance requirements. + +!!! info "Full Markdown Support (v0.6.2+)" + The recording disclaimer now supports **full markdown formatting**, including: + + - **Headings** - Structure your disclaimer with `# Main Title` and `## Sections` + - **Lists** - Bulleted and numbered lists for clear requirements + - **Bold and Italic** - Emphasize important terms with `**bold**` or `*italic*` + - **Links** - Reference detailed policies with `[Privacy Policy](https://yoursite.com/privacy)` + - **Code blocks** - Include examples or technical requirements + - **Blockquotes** - Highlight key legal notices + + Example markdown disclaimer: + ```markdown + ## Recording Consent Required + + By starting this recording, you agree to: + + 1. Obtain consent from all participants + 2. Comply with [company privacy policy](https://example.com/privacy) + 3. Handle recordings according to **GDPR** requirements + + > **Important**: Recordings containing sensitive information must be deleted within 30 days. + ``` + +Keep disclaimers concise and relevant. Users see this message frequently, so lengthy legal text becomes an ignored click-through. Focus on the most important points, and link to detailed policies if needed. The markdown support lets you format the message clearly for better readability and comprehension. + +## Upload Disclaimer + +The upload disclaimer works just like the recording disclaimer, but it appears when users upload files rather than when they start recording. Every time a user drags and drops files or selects files for upload, they'll see this notice and must accept it before the files are queued for processing. + +This is useful when uploaded files may contain third-party content or when your organization needs to remind users about data handling before they submit files to the system. The disclaimer supports full markdown formatting, just like the recording disclaimer. + +!!! example "Example upload disclaimer" + ```markdown + ## Upload Policy + + By uploading files, you confirm that: + + - You have the right to share this content + - No sensitive personal data is included without authorization + - Files will be processed by external transcription services + + > See our [data handling policy](https://example.com/policy) for details. + ``` + +Leave this field empty to disable the upload disclaimer entirely. When empty, file uploads proceed immediately without any prompt. + +## Custom Banner + +The custom banner displays a persistent message across the top of the main content area for all users. It's useful for announcements, maintenance notices, compliance reminders, or any message you want everyone to see when they use Speakr. + +The banner appears below the header and above the main content. Users can dismiss it by clicking the X button, but it reappears on page refresh, ensuring the message stays visible as long as you have it configured. + +Like the disclaimers, the banner supports full markdown formatting, so you can include bold text, links, and other formatting. Keep banner text short and to the point since it takes up screen space. + +!!! example "Example banners" + ```markdown + **System update** — Speakr will be briefly unavailable on Sunday 10pm-12am for maintenance. + ``` + + ```markdown + All recordings are subject to our [acceptable use policy](https://example.com/aup). Contact IT with questions. + ``` + +Leave this field empty to hide the banner completely. + +## System-Wide Impact + +Every setting on this page affects all users immediately. Changes take effect as soon as you save them, without requiring system restarts or user logouts. This immediate application means you should test changes carefully and communicate significant modifications to your users. + +The refresh button reloads settings from the database, useful if multiple admins might be making changes or if you want to ensure you're seeing the latest values. The interface shows when each setting was last updated, helping you track changes over time. + +## Troubleshooting Common Issues + +When recordings fail consistently, check if they're hitting your configured limits. The error logs will indicate if files are too large or if processing is timing out. Users might not realize their recordings exceed limits, especially if they're uploading existing content rather than recording directly. + +If API costs spike unexpectedly, review your transcript length limit. A single user uploading many long recordings could dramatically increase consumption if no limit is set. The combination of user activity and system settings determines your actual costs. + +Processing backlogs might indicate your timeout is too high. If the system waits 30 minutes for each failed transcription attempt, a series of problematic files could block the queue for hours. Balance patience for slow processing with the need to fail fast when services are actually down. + +## Environment Variable Configuration + +Beyond the UI-configurable settings above, several environment variables in your `.env` file control fundamental system behaviors. These require instance restart to take effect. + +### Collaboration & Sharing + +**ENABLE_INTERNAL_SHARING**: Controls user-to-user sharing capabilities. Set to `true` to enable internal sharing features, allowing users to share recordings with specific colleagues. Required for group functionality. Default: `false`. + +**SHOW_USERNAMES_IN_UI**: Controls username visibility in the interface. When `true`, usernames are displayed throughout the UI when sharing and collaborating. When `false`, usernames are hidden - users must know each other's usernames to share recordings (they type the username manually). Default: `false`. + +**ENABLE_PUBLIC_SHARING**: Controls whether public share links can be created. When `true`, authorized users can generate secure links for external sharing. When `false`, only internal sharing is available. Default: `false`. + +### User Permissions + +**USERS_CAN_DELETE**: Determines whether regular users can delete their own recordings. When `true`, users see delete buttons for their recordings. When `false`, only administrators can delete recordings. This helps prevent accidental data loss and maintains content retention for compliance. Default: `true`. + +### Retention & Auto-Deletion + +**ENABLE_AUTO_DELETION**: Enables the automated retention system. When `true`, recordings older than the retention period are automatically processed for deletion. Default: `false`. + +**DEFAULT_RETENTION_DAYS**: Global retention period in days for recordings without tag-specific retention. Set to `0` to disable auto-deletion. Tag-level retention policies can override this default. Default: `0` (disabled). + +**DELETION_MODE**: Controls what gets deleted: `audio_only` removes audio files but preserves transcriptions and metadata, while `full_recording` removes everything. Audio-only mode maintains searchable records while saving storage space. Default: `audio_only`. + +For detailed retention configuration, see the [Retention & Auto-Deletion](retention.md) guide. + +### Background Processing Queues + +Speakr uses separate job queues for transcription and summarization to prevent slow ASR processing from blocking quick summary generation. + +**JOB_QUEUE_WORKERS**: Number of workers for transcription jobs (ASR processing). These are slow jobs that can take 5-30 minutes. Default: `2`. + +**SUMMARY_QUEUE_WORKERS**: Number of workers for summary jobs (LLM API calls). These are fast jobs that typically complete in under a minute. Default: `2`. + +**JOB_MAX_RETRIES**: How many times a failed job will be retried before being marked as failed. Default: `3`. + +Jobs are persisted to the database and survive application restarts. If Speakr restarts while jobs are processing, they automatically resume from where they left off. + +### Folders Feature + +**ENABLE_FOLDERS**: Enable the folders organization feature. When `true`, users can create folders to organize recordings with per-folder custom prompts and ASR settings. Default: `false`. + +### Public Share Page Rendering + +**READABLE_PUBLIC_LINKS**: When `true`, transcripts on public share pages are server-side rendered in HTML, making them accessible to LLMs, scrapers, and accessibility tools. When `false`, transcripts are rendered client-side via JavaScript. Default: `false`. + +### Admin User Creation + +**SKIP_EMAIL_DOMAIN_CHECK**: When `true`, bypasses DNS validation of email domains when creating admin users via the setup script. Useful for development or when DNS lookups are restricted. Default: `false`. + +### Speaker Profile Cleanup + +**DELETE_ORPHANED_SPEAKERS**: Controls whether speaker profiles are automatically deleted when all their associated recordings are removed. When `false` (the default), speaker profiles and voice embeddings are preserved. When `true`, speakers with no remaining recordings are automatically cleaned up. Default: `false`. + +### Video Retention + +**VIDEO_RETENTION**: When enabled, uploaded video files keep their video stream for in-browser playback instead of extracting audio and discarding the video. The audio is extracted to a temporary file for transcription only, then cleaned up after processing. The video renders with a native `