Skip to content

updates (transcription, pii redaction, clone, duration bug)#330

Merged
spashii merged 6 commits intomainfrom
wip-pre-H
Oct 12, 2025
Merged

updates (transcription, pii redaction, clone, duration bug)#330
spashii merged 6 commits intomainfrom
wip-pre-H

Conversation

@spashii
Copy link
Copy Markdown
Member

@spashii spashii commented Oct 12, 2025

  • (feature) new "Dembrane 25-09" transcription provider used for all new transcription (AssemblyAI + Gemini)
  • (feature) updated retranscribe flow (pii redaction, toast that takes you to new conversation)
  • (feature) add clone project functionality (only metadata) + tests
  • (bug) fix premature calling of "finish conversation" (duration bug)
  • dev stuff:
    • use vertex ai instead of gemini api
    • remove grid view
    • add biome for frontend linting and formatting (eslint + prettier was breaking my pc lol)
    • refactor conversation transcript into maintainable components
  • side effects:
    • remember to add GCP_SA_JSON env var. ask @spashii for it.

Summary by CodeRabbit

  • New Features
    • Clone projects, with optional name/language.
    • Export all transcripts as a ZIP from project settings.
    • Copy or download a conversation transcript; retranscribe with optional PII redaction.
    • View conversation status in a modal within project settings.
  • Refactor
    • Streamlined conversation transcript page with infinite scroll and a simpler audio/transcript view.
    • Revamped conversation list UI with improved filters, search, and loading states.
    • Projects Home now always uses a list view (grid removed).
  • Performance
    • More reliable uploads via presigned URLs.
  • Chores
    • Switched formatting/tooling to Biome.
  • Tests
    • Added coverage for project cloning.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Oct 12, 2025

Walkthrough

Frontend adopts Biome, refactors conversation and project UIs, adds transcript copy/download/retranscribe features, and simplifies transcript chunk rendering. Backend adds project cloning, PII redaction through transcription pipeline, Vertex AI integration, config updates, and scheduler guards. New tests added. Various scripts, settings, and configs updated.

Changes

Cohort / File(s) Change summary
Dev environment & VSCode
echo/.devcontainer/devcontainer.json, echo/.vscode/sessions.json, echo/.vscode/settings.json
Swap Prettier/ESLint extensions for Biome; restructure terminal sessions into grouped sets; set Biome as formatter and code-actions on save, remove ESLint settings.
Docs
echo/check-later.md
Added notes on workers, devcontainer, Python imports, PR size, Dockerfile.
Frontend tooling
echo/frontend/biome.json, echo/frontend/package.json
Add Biome config and devDependency.
Conversation UI & hooks
echo/frontend/src/components/conversation/ConversationAccordion.tsx, .../ConversationChunkAudioTranscript.tsx, .../CopyConversationTranscript.tsx, .../DownloadConversationTranscript.tsx, .../RetranscribeConversation.tsx, .../hooks/index.ts, echo/frontend/src/routes/project/conversation/ProjectConversationTranscript.tsx
Large refactors to hook-driven data flows, new actions (copy/download/retranscribe), simplified chunk transcript component/signature, infinite pagination tweaks, hook renames (introduce useGetConversationTranscriptStringMutation), UI/state reorganization.
Project UI & hooks
echo/frontend/src/components/project/ProjectSettingsSection.tsx, .../ProjectBasicEdit.tsx, .../ProjectConversationStatusSection.tsx, .../ProjectDangerZone.tsx, .../ProjectExportSection.tsx, .../ProjectUploadSection.tsx, .../ProjectListSkeleton.tsx, echo/frontend/src/components/project/hooks/index.ts, echo/frontend/src/routes/project/ProjectRoutes.tsx, .../ProjectsHome.tsx
Introduce reusable settings section; add export/upload/status sections; add project clone/delete modals; add clone mutation hook; remove grid view and simplify skeleton props; restructure project route to use new sections.
Frontend API
echo/frontend/src/lib/api.ts
Normalize typings/formatting; add cloneProjectById export; maintain existing endpoints; adjust type imports and Directus helpers.
Server APIs & services
echo/server/dembrane/api/project.py, .../api/conversation.py, .../service/project.py, echo/server/tests/service/test_project_service.py
Add project clone endpoint and request schema; add use_pii_redaction to retranscribe body; implement create_shallow_clone and enhanced create; add test covering shallow clone with tags.
Transcription pipeline & config
echo/server/dembrane/transcribe.py, .../tasks.py, .../prompt_templates/transcript_correction_workflow.en.jinja, .../config.py, .../scheduler.py, .../conversation_utils.py
Propagate PII redaction flag across tasks/transcribe; switch to Vertex AI creds (GCP_SA_JSON) for Gemini; overhaul correction prompt; conditional scheduling based on TRANSCRIPTION_PROVIDER; exclude very recent conversations in unfinished collector.
Server runtime scripts
echo/server/prod-worker.sh, echo/server/run-worker.sh, echo/server/run-worker-cpu.sh
Adjust dramatiq watch flags and process/thread counts; enable watching dembrane in dev workers.
Server dependencies
echo/server/pyproject.toml
Add google-cloud-aiplatform dependency.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

Feature, bug, improvement

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.63% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title Check ❓ Inconclusive The title “updates (transcription, pii redaction, clone, duration bug)” relies on the generic term “updates” and then lists multiple changes, making it too vague to immediately convey the main focus of the pull request. Please revise the title to a concise sentence that highlights the primary change—for example, “Use Vertex AI for transcription with PII redaction, add project clone endpoint, and fix conversation duration bug.”
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch wip-pre-H

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

formatting and biome.

@spashii spashii requested a review from ussaama October 12, 2025 22:56
cursor[bot]

This comment was marked as outdated.

@coderabbitai coderabbitai bot added bug Something isn't working Feature improvement labels Oct 12, 2025
@spashii spashii merged commit 22621e3 into main Oct 12, 2025
7 of 9 checks passed
@spashii spashii deleted the wip-pre-H branch October 12, 2025 22:58
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
echo/frontend/src/routes/project/ProjectsHome.tsx (1)

47-49: Drop the orphaned gridParent ref.
We nuked the grid view but still spin up a second useAutoAnimate() ref that never gets used—let’s delete it and keep the hook count tight.

-  const [gridParent] = useAutoAnimate();
   const [listParent] = useAutoAnimate();
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c684a25 and c8929aa.

⛔ Files ignored due to path filters (3)
  • echo/frontend/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • echo/server/requirements-dev.lock is excluded by !**/*.lock
  • echo/server/requirements.lock is excluded by !**/*.lock
📒 Files selected for processing (38)
  • echo/.devcontainer/devcontainer.json (1 hunks)
  • echo/.vscode/sessions.json (1 hunks)
  • echo/.vscode/settings.json (1 hunks)
  • echo/check-later.md (1 hunks)
  • echo/frontend/biome.json (1 hunks)
  • echo/frontend/package.json (1 hunks)
  • echo/frontend/src/components/conversation/ConversationAccordion.tsx (4 hunks)
  • echo/frontend/src/components/conversation/ConversationChunkAudioTranscript.tsx (1 hunks)
  • echo/frontend/src/components/conversation/CopyConversationTranscript.tsx (1 hunks)
  • echo/frontend/src/components/conversation/DownloadConversationTranscript.tsx (1 hunks)
  • echo/frontend/src/components/conversation/RetranscribeConversation.tsx (1 hunks)
  • echo/frontend/src/components/conversation/hooks/index.ts (1 hunks)
  • echo/frontend/src/components/project/ProjectBasicEdit.tsx (2 hunks)
  • echo/frontend/src/components/project/ProjectConversationStatusSection.tsx (1 hunks)
  • echo/frontend/src/components/project/ProjectDangerZone.tsx (1 hunks)
  • echo/frontend/src/components/project/ProjectExportSection.tsx (1 hunks)
  • echo/frontend/src/components/project/ProjectListSkeleton.tsx (1 hunks)
  • echo/frontend/src/components/project/ProjectSettingsSection.tsx (1 hunks)
  • echo/frontend/src/components/project/ProjectUploadSection.tsx (1 hunks)
  • echo/frontend/src/components/project/hooks/index.ts (2 hunks)
  • echo/frontend/src/lib/api.ts (1 hunks)
  • echo/frontend/src/routes/project/ProjectRoutes.tsx (2 hunks)
  • echo/frontend/src/routes/project/ProjectsHome.tsx (2 hunks)
  • echo/frontend/src/routes/project/conversation/ProjectConversationTranscript.tsx (1 hunks)
  • echo/server/dembrane/api/conversation.py (2 hunks)
  • echo/server/dembrane/api/project.py (2 hunks)
  • echo/server/dembrane/config.py (2 hunks)
  • echo/server/dembrane/conversation_utils.py (2 hunks)
  • echo/server/dembrane/scheduler.py (2 hunks)
  • echo/server/dembrane/service/project.py (4 hunks)
  • echo/server/dembrane/tasks.py (13 hunks)
  • echo/server/dembrane/transcribe.py (9 hunks)
  • echo/server/prod-worker.sh (0 hunks)
  • echo/server/prompt_templates/transcript_correction_workflow.en.jinja (1 hunks)
  • echo/server/pyproject.toml (1 hunks)
  • echo/server/run-worker-cpu.sh (1 hunks)
  • echo/server/run-worker.sh (1 hunks)
  • echo/server/tests/service/test_project_service.py (1 hunks)
💤 Files with no reviewable changes (1)
  • echo/server/prod-worker.sh
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-10-07T10:59:13.893Z
Learnt from: dtrn2048
PR: Dembrane/echo#322
File: echo/server/run-worker-cpu.sh:3-3
Timestamp: 2025-10-07T10:59:13.893Z
Learning: In the echo repository, run-worker-cpu.sh is the local development worker script and can use fewer processes (e.g., --processes 2), while prod-worker-cpu.sh is the production worker script that should use --processes 8 --threads 1 for LightRAG processing due to its internal locking requirements.

Applied to files:

  • echo/server/run-worker-cpu.sh
📚 Learning: 2025-08-19T10:22:55.323Z
Learnt from: ussaama
PR: Dembrane/echo#266
File: echo/frontend/src/components/conversation/ConversationAccordion.tsx:675-678
Timestamp: 2025-08-19T10:22:55.323Z
Learning: In echo/frontend/src/components/conversation/hooks/index.ts, the useConversationsCountByProjectId hook uses useSuspenseQuery with Directus aggregate, which always returns string numbers like "0", "1", "2" and suspends during loading instead of returning undefined. Therefore, Number(conversationsCountQuery.data) ?? 0 is safe and the Number() conversion is necessary for type conversion from string to number.

Applied to files:

  • echo/frontend/src/components/conversation/hooks/index.ts
📚 Learning: 2025-08-19T10:22:55.323Z
Learnt from: ussaama
PR: Dembrane/echo#266
File: echo/frontend/src/components/conversation/ConversationAccordion.tsx:675-678
Timestamp: 2025-08-19T10:22:55.323Z
Learning: In echo/frontend/src/components/conversation/hooks/index.ts, the useConversationsCountByProjectId hook uses regular useQuery (not useSuspenseQuery), which means conversationsCountQuery.data can be undefined during loading states. When using Number(conversationsCountQuery.data) ?? 0, this creates NaN because Number(undefined) = NaN and NaN is not nullish, so the fallback doesn't apply. The correct pattern is Number(conversationsCountQuery.data ?? 0) to ensure the fallback happens before type conversion.

Applied to files:

  • echo/frontend/src/components/conversation/hooks/index.ts
🧬 Code graph analysis (22)
echo/server/dembrane/api/conversation.py (1)
echo/server/dembrane/tasks.py (1)
  • task_process_conversation_chunk (512-560)
echo/server/tests/service/test_project_service.py (1)
echo/server/dembrane/service/project.py (5)
  • create (56-76)
  • create_tags_and_link (85-110)
  • create_shallow_clone (112-180)
  • get_by_id_or_raise (22-54)
  • delete (78-83)
echo/frontend/src/components/project/ProjectConversationStatusSection.tsx (2)
echo/frontend/src/components/project/ProjectSettingsSection.tsx (1)
  • ProjectSettingsSection (21-55)
echo/frontend/src/components/report/ConversationStatusTable.tsx (1)
  • ConversationStatusTable (15-101)
echo/frontend/src/components/conversation/CopyConversationTranscript.tsx (1)
echo/frontend/src/components/conversation/hooks/index.ts (1)
  • useGetConversationTranscriptStringMutation (621-626)
echo/frontend/src/components/project/hooks/index.ts (1)
echo/frontend/src/lib/api.ts (1)
  • cloneProjectById (223-245)
echo/server/dembrane/api/project.py (1)
echo/server/dembrane/service/project.py (1)
  • create_shallow_clone (112-180)
echo/frontend/src/components/project/ProjectUploadSection.tsx (2)
echo/frontend/src/components/project/ProjectSettingsSection.tsx (1)
  • ProjectSettingsSection (21-55)
echo/frontend/src/components/dropzone/UploadConversationDropzone.tsx (1)
  • UploadConversationDropzone (248-815)
echo/server/dembrane/conversation_utils.py (1)
echo/server/dembrane/utils.py (1)
  • get_utc_timestamp (49-50)
echo/frontend/src/components/conversation/DownloadConversationTranscript.tsx (1)
echo/frontend/src/components/conversation/hooks/index.ts (1)
  • useGetConversationTranscriptStringMutation (621-626)
echo/server/dembrane/transcribe.py (1)
echo/server/dembrane/s3.py (1)
  • get_signed_url (143-151)
echo/server/dembrane/tasks.py (4)
echo/server/dembrane/transcribe.py (1)
  • transcribe_conversation_chunk (497-578)
echo/server/dembrane/api/stateless.py (2)
  • InsertRequest (78-81)
  • insert_item (90-128)
echo/server/dembrane/audio_lightrag/utils/async_utils.py (1)
  • run_async_in_new_loop (59-95)
echo/server/dembrane/audio_lightrag/services/contextualizer.py (2)
  • get_contextualizer (97-102)
  • contextualize (18-90)
echo/frontend/src/components/conversation/RetranscribeConversation.tsx (2)
echo/frontend/src/components/conversation/hooks/index.ts (1)
  • useRetranscribeConversationMutation (590-619)
echo/frontend/src/hooks/useI18nNavigate.ts (1)
  • useI18nNavigate (5-33)
echo/frontend/src/components/project/ProjectExportSection.tsx (1)
echo/frontend/src/components/project/ProjectSettingsSection.tsx (1)
  • ProjectSettingsSection (21-55)
echo/frontend/src/components/project/ProjectBasicEdit.tsx (1)
echo/frontend/src/components/project/ProjectSettingsSection.tsx (1)
  • ProjectSettingsSection (21-55)
echo/frontend/src/components/conversation/ConversationChunkAudioTranscript.tsx (2)
echo/frontend/src/components/conversation/hooks/index.ts (1)
  • useConversationChunkContentUrl (553-572)
echo/frontend/src/components/chat/BaseMessage.tsx (1)
  • BaseMessage (14-55)
echo/frontend/src/routes/project/ProjectsHome.tsx (1)
echo/frontend/src/components/project/ProjectListSkeleton.tsx (1)
  • ProjectListSkeleton (9-38)
echo/frontend/src/routes/project/ProjectRoutes.tsx (3)
echo/frontend/src/components/project/ProjectUploadSection.tsx (1)
  • ProjectUploadSection (10-28)
echo/frontend/src/components/project/ProjectExportSection.tsx (1)
  • ProjectExportSection (11-36)
echo/frontend/src/lib/api.ts (1)
  • getProjectTranscriptsLink (220-221)
echo/frontend/src/components/conversation/hooks/index.ts (2)
echo/frontend/src/lib/directus.ts (1)
  • directus (6-14)
echo/frontend/src/lib/api.ts (8)
  • deleteConversationById (1081-1090)
  • addChatContext (940-952)
  • deleteChatContext (954-966)
  • getConversationChunkContentLink (885-890)
  • apiNoAuth (19-19)
  • getConversationContentLink (877-883)
  • retranscribeConversation (922-934)
  • getConversationTranscriptString (914-920)
echo/frontend/src/routes/project/conversation/ProjectConversationTranscript.tsx (5)
echo/frontend/src/components/conversation/hooks/index.ts (2)
  • useConversationById (783-836)
  • useInfiniteConversationChunks (34-83)
echo/frontend/src/components/conversation/DownloadConversationTranscript.tsx (1)
  • DownloadConversationTranscriptModalActionIcon (16-37)
echo/frontend/src/components/conversation/CopyConversationTranscript.tsx (1)
  • CopyConversationTranscriptActionIcon (7-47)
echo/frontend/src/components/conversation/RetranscribeConversation.tsx (1)
  • RetranscribeConversationModalActionIcon (25-50)
echo/frontend/src/components/conversation/ConversationChunkAudioTranscript.tsx (1)
  • ConversationChunkAudioTranscript (7-78)
echo/frontend/src/components/conversation/ConversationAccordion.tsx (9)
echo/frontend/src/components/chat/hooks/index.ts (1)
  • useProjectChatContext (111-117)
echo/frontend/src/components/conversation/hooks/index.ts (5)
  • useAddChatContextMutation (282-413)
  • useDeleteChatContextMutation (415-551)
  • useMoveConversationMutation (251-280)
  • useInfiniteConversationsByProjectId (838-957)
  • useConversationsCountByProjectId (959-984)
echo/frontend/src/components/project/hooks/index.ts (2)
  • useInfiniteProjects (215-248)
  • useProjectById (250-275)
echo/frontend/src/components/common/NavigationButton.tsx (1)
  • NavigationButton (28-118)
echo/frontend/src/lib/utils.ts (1)
  • cn (4-6)
echo/frontend/src/components/dropzone/UploadConversationDropzone.tsx (1)
  • UploadConversationDropzone (248-815)
echo/frontend/src/components/conversation/AutoSelectConversations.tsx (1)
  • AutoSelectConversations (27-193)
echo/frontend/src/components/common/i18nLink.tsx (1)
  • I18nLink (6-26)
echo/frontend/src/components/common/BaseSkeleton.tsx (1)
  • BaseSkeleton (11-25)
echo/frontend/src/components/project/ProjectDangerZone.tsx (3)
echo/frontend/src/components/project/hooks/index.ts (2)
  • useDeleteProjectByIdMutation (25-38)
  • useCloneProjectByIdMutation (40-71)
echo/frontend/src/hooks/useI18nNavigate.ts (1)
  • useI18nNavigate (5-33)
echo/frontend/src/components/project/ProjectSettingsSection.tsx (1)
  • ProjectSettingsSection (21-55)
echo/frontend/src/lib/api.ts (3)
echo/frontend/src/config.ts (2)
  • API_BASE_URL (7-7)
  • USE_PARTICIPANT_ROUTER (1-2)
echo/frontend/src/lib/directus.ts (2)
  • directusContent (19-21)
  • directus (6-14)
echo/frontend/src/lib/typesDirectusContent.ts (1)
  • EchoPortalTutorial (753-764)
🪛 markdownlint-cli2 (0.18.1)
echo/check-later.md

1-1: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Cursor Bugbot
  • GitHub Check: ci-check-server
🔇 Additional comments (9)
echo/check-later.md (1)

1-7: LGTM!

Internal notes file for tracking TODO items. No functional concerns here.

echo/frontend/package.json (1)

88-88: LGTM!

Biome dependency addition aligns with the tooling migration. Clean integration with the rest of the biome config stack.

echo/frontend/biome.json (1)

1-37: LGTM!

Solid biome config. The useUniqueElementIds exception is reasonable for dynamic React components.

echo/.vscode/settings.json (1)

24-40: LGTM!

Biome integration is clean. Format-on-save and code actions are properly configured.

echo/frontend/src/components/project/hooks/index.ts (1)

40-71: LGTM!

Mutation hook is well-structured with proper query invalidation and error handling. The success path correctly invalidates the projects list, source project, and new project queries to ensure fresh data.

echo/server/dembrane/service/project.py (2)

56-76: LGTM!

The create method enhancement with **kwargs is clean and flexible. Proper propagation of extra fields to Directus.


112-180: Verify cloned project ownership.

The create_shallow_clone method copies directus_user_id from the source project (line 129), meaning the cloned project will have the same owner as the original. This might not be the intended behavior if users are cloning projects they don't own.

Consider whether the clone should:

  1. Inherit the original owner (current behavior)
  2. Be owned by the user performing the clone
  3. Be configurable via overrides

If option 2 is preferred, the API endpoint should pass the authenticated user's ID as an override.

echo/server/pyproject.toml (1)

91-91: No known vulnerabilities in google-cloud-aiplatform 1.120.*; LGTM.

echo/frontend/src/components/conversation/ConversationAccordion.tsx (1)

688-691: Fix the conversations count fallback

Number(conversationsCountQuery.data) ?? 0 returns NaN during the loading path because Number(undefined) is NaN, and NaN isn’t nullish so the ?? never fires. That breaks the “Conversations” badge and anything relying on this count. Coalesce before casting, e.g. Number(conversationsCountQuery.data ?? 0). Based on learnings.

⛔ Skipped due to learnings
Learnt from: ussaama
PR: Dembrane/echo#266
File: echo/frontend/src/components/conversation/ConversationAccordion.tsx:675-678
Timestamp: 2025-08-19T10:22:55.323Z
Learning: In echo/frontend/src/components/conversation/hooks/index.ts, the useConversationsCountByProjectId hook uses regular useQuery (not useSuspenseQuery), which means conversationsCountQuery.data can be undefined during loading states. When using Number(conversationsCountQuery.data) ?? 0, this creates NaN because Number(undefined) = NaN and NaN is not nullish, so the fallback doesn't apply. The correct pattern is Number(conversationsCountQuery.data ?? 0) to ensure the fallback happens before type conversion.
Learnt from: ussaama
PR: Dembrane/echo#266
File: echo/frontend/src/components/conversation/ConversationAccordion.tsx:675-678
Timestamp: 2025-08-19T10:22:55.323Z
Learning: In echo/frontend/src/components/conversation/hooks/index.ts, the useConversationsCountByProjectId hook uses useSuspenseQuery with Directus aggregate, which always returns string numbers like "0", "1", "2" and suspends during loading instead of returning undefined. Therefore, Number(conversationsCountQuery.data) ?? 0 is safe and the Number() conversion is necessary for type conversion from string to number.

Comment on lines +17 to +40
const preCopy = useCallback(async () => {
getConversationTranscriptStringMutation.mutate(conversationId, {
onSuccess: (data) => {
setTranscript(data);
},
});
}, [getConversationTranscriptStringMutation, conversationId]);

return (
<CopyButton value={transcript}>
{({ copied, copy }) => (
<Tooltip label={copied ? t`Copied` : t`Copy to clipboard`}>
<ActionIcon
variant="transparent"
color={copied ? "blue" : "gray"}
onClick={async () => {
await preCopy();
// hmm this is a hack to wait for the transcript to be set. not rly a best practice
// i rly wanted to use the CopyButton haha
await new Promise((resolve) => setTimeout(resolve, 500));
copy();
}}
disabled={getConversationTranscriptStringMutation.isPending}
>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Kill the 500 ms sleep hack

We’re firing mutate and then praying the transcript lands within 500 ms before calling copy(). On a slow round-trip—or on error—we end up copying an empty/stale string. Let’s await the fetch with mutateAsync and pass the resolved payload straight into copy(), no timers, no races.

- const [transcript, setTranscript] = useState<string>("");
-
- const preCopy = useCallback(async () => {
-   getConversationTranscriptStringMutation.mutate(conversationId, {
-     onSuccess: (data) => {
-       setTranscript(data);
-     },
-   });
- }, [getConversationTranscriptStringMutation, conversationId]);
+ const [transcript, setTranscript] = useState<string>("");
@@
-          onClick={async () => {
-            await preCopy();
-            // hmm this is a hack to wait for the transcript to be set. not rly a best practice
-            // i rly wanted to use the CopyButton haha
-            await new Promise((resolve) => setTimeout(resolve, 500));
-            copy();
-          }}
+          onClick={async () => {
+            try {
+              const data =
+                await getConversationTranscriptStringMutation.mutateAsync(
+                  conversationId,
+                );
+              setTranscript(data);
+              copy(data);
+            } catch (err) {
+              console.error(err);
+            }
+          }}
🤖 Prompt for AI Agents
In echo/frontend/src/components/conversation/CopyConversationTranscript.tsx
around lines 17 to 40, remove the 500ms sleep hack and instead await the server
response: replace the current mutate call with mutateAsync (or call
getConversationTranscriptStringMutation.mutateAsync directly in the onClick),
await the resolved transcript payload, then call copy(resolvedTranscript) (or
setTranscript(resolvedTranscript) and then call copy with that value). Ensure
you remove the setTimeout and the separate preCopy wait, propagate errors (or
handle them) and keep the button disabled while
getConversationTranscriptStringMutation.isPending to avoid races.

Comment on lines +254 to +278
mutationFn: async ({
conversationId,
targetProjectId,
}: {
conversationId: string;
targetProjectId: string;
}) => {
try {
await directus.request(
updateItem("conversation", conversationId, {
project_id: targetProjectId,
}),
);
} catch (error) {
toast.error("Failed to move conversation.");
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["conversations"] });
queryClient.invalidateQueries({ queryKey: ["projects"] });
toast.success("Conversation moved successfully");
},
onError: (error: Error) => {
toast.error("Failed to move conversation: " + error.message);
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Don’t swallow the move-conversation failure

mutationFn catches the Directus failure, shows a toast, and then quietly resolves. React Query sees that as “success”, so onSuccess and the success toast still fire. That’s a hard false-positive. Let the error propagate (or rethrow) so onError handles it and onSuccess stays dark.

-    mutationFn: async ({
+    mutationFn: async ({
         conversationId,
         targetProjectId,
       }: {
         conversationId: string;
         targetProjectId: string;
       }) => {
-      try {
-        await directus.request(
-          updateItem("conversation", conversationId, {
-            project_id: targetProjectId,
-          }),
-        );
-      } catch (error) {
-        toast.error("Failed to move conversation.");
-      }
+      await directus.request(
+        updateItem("conversation", conversationId, {
+          project_id: targetProjectId,
+        }),
+      );
     },
     onSuccess: () => {
       queryClient.invalidateQueries({ queryKey: ["conversations"] });
       queryClient.invalidateQueries({ queryKey: ["projects"] });
       toast.success("Conversation moved successfully");
     },
     onError: (error: Error) => {
       toast.error("Failed to move conversation: " + error.message);
     },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
mutationFn: async ({
conversationId,
targetProjectId,
}: {
conversationId: string;
targetProjectId: string;
}) => {
try {
await directus.request(
updateItem("conversation", conversationId, {
project_id: targetProjectId,
}),
);
} catch (error) {
toast.error("Failed to move conversation.");
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["conversations"] });
queryClient.invalidateQueries({ queryKey: ["projects"] });
toast.success("Conversation moved successfully");
},
onError: (error: Error) => {
toast.error("Failed to move conversation: " + error.message);
},
mutationFn: async ({
conversationId,
targetProjectId,
}: {
conversationId: string;
targetProjectId: string;
}) => {
await directus.request(
updateItem("conversation", conversationId, {
project_id: targetProjectId,
}),
);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["conversations"] });
queryClient.invalidateQueries({ queryKey: ["projects"] });
toast.success("Conversation moved successfully");
},
onError: (error: Error) => {
toast.error("Failed to move conversation: " + error.message);
},
🤖 Prompt for AI Agents
In echo/frontend/src/components/conversation/hooks/index.ts around lines
254-278, the mutationFn currently catches Directus errors, shows a toast, and
returns normally which makes React Query treat the operation as a success;
remove the swallowing behavior so the error propagates: either drop the
try/catch entirely or rethrow the caught error after calling toast.error (and
avoid duplicating success/failure toasts in both places), so that React Query
calls onError instead of onSuccess when the request fails.

Comment on lines +75 to +101
const handleRetranscribe = async () => {
if (!conversationId || !newConversationName.trim()) return;
const { new_conversation_id } = await retranscribeMutation.mutateAsync({
conversationId,
newConversationName: newConversationName.trim(),
usePiiRedaction,
});
if (new_conversation_id) {
onClose();
toast.success(
t`Retranscription started. New conversation will be available soon.`,
{
action: {
label: t`Go to new conversation`,
actionButtonStyle: {
color: "blue",
},
onClick: () => {
navigate(
`/projects/${projectId}/conversations/${new_conversation_id}/transcript`,
);
},
},
},
);
}
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Catch the mutateAsync rejection

mutateAsync still rejects after onError fires, so this async handler bubbles an unhandled rejection straight into React, spamming the console and tripping test suites. Wrap the call in try/catch (or use void retranscribeMutation.mutate) so we keep the toast but don’t crash the click path.

  const handleRetranscribe = async () => {
-    if (!conversationId || !newConversationName.trim()) return;
-    const { new_conversation_id } = await retranscribeMutation.mutateAsync({
-      conversationId,
-      newConversationName: newConversationName.trim(),
-      usePiiRedaction,
-    });
+    if (!conversationId || !newConversationName.trim()) return;
+    try {
+      const { new_conversation_id } = await retranscribeMutation.mutateAsync({
+        conversationId,
+        newConversationName: newConversationName.trim(),
+        usePiiRedaction,
+      });
+      if (new_conversation_id) {
+        onClose();
+        toast.success(
+          t`Retranscription started. New conversation will be available soon.`,
+          {
+            action: {
+              label: t`Go to new conversation`,
+              actionButtonStyle: {
+                color: "blue",
+              },
+              onClick: () => {
+                navigate(
+                  `/projects/${projectId}/conversations/${new_conversation_id}/transcript`,
+                );
+              },
+            },
+          },
+        );
+      }
+    } catch {
+      // no-op: toast already fired in onError
+    }
-    if (new_conversation_id) {
-      onClose();
-      toast.success(
-        t`Retranscription started. New conversation will be available soon.`,
-        {
-          action: {
-            label: t`Go to new conversation`,
-            actionButtonStyle: {
-              color: "blue",
-            },
-            onClick: () => {
-              navigate(
-                `/projects/${projectId}/conversations/${new_conversation_id}/transcript`,
-              );
-            },
-          },
-        },
-      );
-    }
  };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleRetranscribe = async () => {
if (!conversationId || !newConversationName.trim()) return;
const { new_conversation_id } = await retranscribeMutation.mutateAsync({
conversationId,
newConversationName: newConversationName.trim(),
usePiiRedaction,
});
if (new_conversation_id) {
onClose();
toast.success(
t`Retranscription started. New conversation will be available soon.`,
{
action: {
label: t`Go to new conversation`,
actionButtonStyle: {
color: "blue",
},
onClick: () => {
navigate(
`/projects/${projectId}/conversations/${new_conversation_id}/transcript`,
);
},
},
},
);
}
};
const handleRetranscribe = async () => {
if (!conversationId || !newConversationName.trim()) return;
try {
const { new_conversation_id } = await retranscribeMutation.mutateAsync({
conversationId,
newConversationName: newConversationName.trim(),
usePiiRedaction,
});
if (new_conversation_id) {
onClose();
toast.success(
t`Retranscription started. New conversation will be available soon.`,
{
action: {
label: t`Go to new conversation`,
actionButtonStyle: {
color: "blue",
},
onClick: () => {
navigate(
`/projects/${projectId}/conversations/${new_conversation_id}/transcript`,
);
},
},
},
);
}
} catch {
// no-op: toast already fired in onError
}
};
🤖 Prompt for AI Agents
In echo/frontend/src/components/conversation/RetranscribeConversation.tsx around
lines 75 to 101, the async handler calls retranscribeMutation.mutateAsync
without catching rejections which lets errors bubble into React; wrap the
mutateAsync call in a try/catch (or replace with void
retranscribeMutation.mutate) so rejection is handled locally: call mutateAsync
inside try, proceed with onClose and toast only on successful response, and
handle/log the error in catch (e.g., show toast.error or noop) to prevent
unhandled promise rejections.

Comment on lines +48 to 56
const handleDelete = () => {
if (
window.confirm(
t`By deleting this project, you will delete all the data associated with it. This action cannot be undone. Are you ABSOLUTELY sure you want to delete this project?`,
)
) {
deleteProjectByIdMutation.mutate(project.id);
navigate(`/projects`);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Do not bail to /projects before the delete mutation resolves

Right now we fire mutate and instantly navigate away. If the delete blows up (network, auth, etc.) we still leave the page and the project survives, leaving stale UI + false “success” toast. Wait for the async to finish (use mutateAsync) and only redirect on success; keep the modal open otherwise.

-  const handleDelete = () => {
-    if (
-      window.confirm(
-        t`By deleting this project, you will delete all the data associated with it. This action cannot be undone. Are you ABSOLUTELY sure you want to delete this project?`,
-      )
-    ) {
-      deleteProjectByIdMutation.mutate(project.id);
-      navigate(`/projects`);
-    }
-  };
+  const handleDelete = async () => {
+    const confirmed = window.confirm(
+      t`By deleting this project, you will delete all the data associated with it. This action cannot be undone. Are you ABSOLUTELY sure you want to delete this project?`,
+    );
+    if (!confirmed) return;
+
+    try {
+      await deleteProjectByIdMutation.mutateAsync(project.id);
+      closeDeleteModal();
+      navigate(`/projects`);
+    } catch {
+      /* mutation hook already surfaces the toast */
+    }
+  };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleDelete = () => {
if (
window.confirm(
t`By deleting this project, you will delete all the data associated with it. This action cannot be undone. Are you ABSOLUTELY sure you want to delete this project?`,
)
) {
deleteProjectByIdMutation.mutate(project.id);
navigate(`/projects`);
}
const handleDelete = async () => {
const confirmed = window.confirm(
t`By deleting this project, you will delete all the data associated with it. This action cannot be undone. Are you ABSOLUTELY sure you want to delete this project?`,
);
if (!confirmed) return;
try {
await deleteProjectByIdMutation.mutateAsync(project.id);
closeDeleteModal();
navigate(`/projects`);
} catch {
/* mutation hook already surfaces the toast */
}
};
🤖 Prompt for AI Agents
In echo/frontend/src/components/project/ProjectDangerZone.tsx around lines 48 to
56, the handler calls deleteProjectByIdMutation.mutate and immediately navigates
away; change it to use mutateAsync and await the promise so navigation only
happens on success. Wrap the await call in a try/catch: on success navigate to
/projects, on failure keep the modal open and show or rethrow the mutation error
(or display an error toast). Also set/clear a local isDeleting state (or use
mutation.isLoading) to disable the UI while awaiting so the user can’t
re-trigger the action.

Comment on lines +11 to +55
type ProjectSettingsSectionProps = {
title: ReactNode;
description?: ReactNode;
headerRight?: ReactNode;
children: ReactNode;
variant?: "default" | "danger";
align?: "start" | "stretch";
id?: string;
};

export const ProjectSettingsSection = ({
title,
description,
headerRight,
children,
align = "stretch",
id,
}: ProjectSettingsSectionProps) => {
return (
<Paper
id={id}
radius="md"
withBorder = {false}
p={{ base: "1.25rem", md: "1.75rem" }}
>
<Stack gap="1.5rem">
<Group justify="space-between" align="flex-start">
<Stack gap="0.4rem">
<Title order={2}>{title}</Title>
{description && (
<Text size="sm" c="dimmed">
{description}
</Text>
)}
</Stack>
{headerRight}
</Group>

<Stack gap="1.25rem" align={align === "start" ? "flex-start" : "stretch"}>
{children}
</Stack>
</Stack>
</Paper>
);
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Wire up the variant prop or remove it before shipping.

Right now we advertise variant?: "default" | "danger" but never branch on it, so passing variant="danger" buys literally nothing. That's a broken promise for anyone expecting destructive sections to stand out. Either implement the styling or retract the prop until it’s real—shipping a dead-on-arrival API is not how we roll.

Here’s the minimal diff to retract it until the styling lands:

-import {
-  Group,
-  Paper,
-  Stack,
-  Text,
-  Title,
-  useMantineTheme,
-} from "@mantine/core";
+import { Group, Paper, Stack, Text, Title } from "@mantine/core";
@@
-  variant?: "default" | "danger";
   align?: "start" | "stretch";
🤖 Prompt for AI Agents
In echo/frontend/src/components/project/ProjectSettingsSection.tsx around lines
11 to 55, the declared prop variant?: "default" | "danger" is never used; remove
it from the public API by deleting it from ProjectSettingsSectionProps, removing
variant from the component destructuring, and deleting any internal references
(none currently) so the prop is no longer accepted; then run TypeScript/grep to
find and fix any call sites passing variant (either remove the prop there or
replace it with an equivalent styling change), and update any related tests or
stories that referenced variant.

Comment on lines +357 to +373
@ProjectRouter.post("/{project_id}/clone")
async def clone_project(project_id: str, body: CloneProjectRequestBodySchema) -> str:
logger.info(f"Cloning project {project_id}")

overrides = {}
if body.name:
overrides["name"] = body.name
if body.language:
overrides["language"] = body.language

new_project_id = project_service.create_shallow_clone(
project_id,
with_tags=True,
**overrides,
)

return new_project_id
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add error handling for service exceptions.

The service call to create_shallow_clone can raise ProjectNotFoundException or ValueError (when id is in overrides), but these aren't caught. This will result in 500 errors instead of proper HTTP responses.

Apply this diff to add error handling:

+from dembrane.service.project import ProjectNotFoundException
+
 @ProjectRouter.post("/{project_id}/clone")
 async def clone_project(project_id: str, body: CloneProjectRequestBodySchema) -> str:
     logger.info(f"Cloning project {project_id}")
 
     overrides = {}
     if body.name:
         overrides["name"] = body.name
     if body.language:
         overrides["language"] = body.language
 
-    new_project_id = project_service.create_shallow_clone(
-        project_id,
-        with_tags=True,
-        **overrides,
-    )
+    try:
+        new_project_id = project_service.create_shallow_clone(
+            project_id,
+            with_tags=True,
+            **overrides,
+        )
+    except ProjectNotFoundException as e:
+        raise HTTPException(status_code=404, detail="Project not found") from e
+    except ValueError as e:
+        raise HTTPException(status_code=400, detail=str(e)) from e
 
     return new_project_id
🤖 Prompt for AI Agents
In echo/server/dembrane/api/project.py around lines 357-373, the call to
project_service.create_shallow_clone can raise ProjectNotFoundException or
ValueError but those exceptions are not handled; wrap the service call in a
try/except that catches ProjectNotFoundException and raises
fastapi.HTTPException(status_code=404, detail="Project not found") and catches
ValueError to raise HTTPException(status_code=400, detail=str(error)) (also log
the errors when caught); ensure you import HTTPException and the
ProjectNotFoundException type at the top of the file and preserve the returned
new_project_id as the endpoint response.

⚠️ Potential issue | 🔴 Critical

Critical: Missing authorization check.

The clone_project endpoint lacks authentication/authorization checks. Any user can clone any project, which is a significant security issue. Endpoints like create_project (line 50) use DependencyDirectusSession for auth, but this endpoint doesn't.

Apply this diff to add authentication and authorization:

 @ProjectRouter.post("/{project_id}/clone")
-async def clone_project(project_id: str, body: CloneProjectRequestBodySchema) -> str:
+async def clone_project(
+    project_id: str, 
+    body: CloneProjectRequestBodySchema,
+    auth: DependencyDirectusSession,
+    db: DependencyInjectDatabase,
+) -> str:
     logger.info(f"Cloning project {project_id}")
+    
+    # Verify project exists and user has access
+    project = db.get(ProjectModel, project_id)
+    if not project:
+        raise HTTPException(status_code=404, detail="Project not found")
+    
+    if not auth.is_admin and project.directus_user_id != auth.user_id:
+        raise HTTPException(status_code=403, detail="User does not have access to this project")
 
     overrides = {}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@ProjectRouter.post("/{project_id}/clone")
async def clone_project(project_id: str, body: CloneProjectRequestBodySchema) -> str:
logger.info(f"Cloning project {project_id}")
overrides = {}
if body.name:
overrides["name"] = body.name
if body.language:
overrides["language"] = body.language
new_project_id = project_service.create_shallow_clone(
project_id,
with_tags=True,
**overrides,
)
return new_project_id
@ProjectRouter.post("/{project_id}/clone")
async def clone_project(
project_id: str,
body: CloneProjectRequestBodySchema,
auth: DependencyDirectusSession,
db: DependencyInjectDatabase,
) -> str:
logger.info(f"Cloning project {project_id}")
# Verify project exists and user has access
project = db.get(ProjectModel, project_id)
if not project:
raise HTTPException(status_code=404, detail="Project not found")
if not auth.is_admin and project.directus_user_id != auth.user_id:
raise HTTPException(status_code=403, detail="User does not have access to this project")
overrides = {}
if body.name:
overrides["name"] = body.name
if body.language:
overrides["language"] = body.language
new_project_id = project_service.create_shallow_clone(
project_id,
with_tags=True,
**overrides,
)
return new_project_id
🤖 Prompt for AI Agents
In echo/server/dembrane/api/project.py around lines 357 to 373, add
authentication and authorization by injecting the DependencyDirectusSession
dependency into the clone_project handler (e.g. a parameter session:
DirectusSession = Depends(DependencyDirectusSession)) so the endpoint requires a
logged-in user; then call the project service authorization check before cloning
(e.g. project_service.authorize_clone(project_id, session) or
project_service.user_can_clone(project_id, session.user) and if it returns false
raise an HTTPException(403)); only proceed to call create_shallow_clone when the
authorization check succeeds and return the new_project_id as before.

Comment on lines +225 to +235
GCP_SA_JSON_RAW = os.environ.get("GCP_SA_JSON")
GCP_SA_JSON = None
try:
if GCP_SA_JSON_RAW:
GCP_SA_JSON = json.loads(GCP_SA_JSON_RAW)
logger.debug("GCP_SA_JSON: set")
else:
logger.debug("GCP_SA_JSON: not set")
except Exception as e:
logger.debug(f"GCP_SA_JSON: not set (invalid json): {e}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fail fast when GCP credentials are bogus.
If GCP_SA_JSON is set but invalid (or missing while we’re on Dembrane-25-09), we now just log at debug and limp forward until Vertex calls explode later. Let’s validate up front and crash loudly, consistent with the rest of this config module.

-GCP_SA_JSON_RAW = os.environ.get("GCP_SA_JSON")
-GCP_SA_JSON = None
-try:
-    if GCP_SA_JSON_RAW:
-        GCP_SA_JSON = json.loads(GCP_SA_JSON_RAW)
-        logger.debug("GCP_SA_JSON: set")
-    else:
-        logger.debug("GCP_SA_JSON: not set")
-except Exception as e:
-    logger.debug(f"GCP_SA_JSON: not set (invalid json): {e}")
+GCP_SA_JSON_RAW = os.environ.get("GCP_SA_JSON")
+GCP_SA_JSON = None
+if GCP_SA_JSON_RAW:
+    try:
+        GCP_SA_JSON = json.loads(GCP_SA_JSON_RAW)
+        logger.debug("GCP_SA_JSON: set")
+    except json.JSONDecodeError as e:
+        raise ValueError("GCP_SA_JSON must be valid JSON") from e
+elif TRANSCRIPTION_PROVIDER == "Dembrane-25-09":
+    raise AssertionError("GCP_SA_JSON environment variable is required for Dembrane-25-09")
+else:
+    logger.debug("GCP_SA_JSON: not set")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
GCP_SA_JSON_RAW = os.environ.get("GCP_SA_JSON")
GCP_SA_JSON = None
try:
if GCP_SA_JSON_RAW:
GCP_SA_JSON = json.loads(GCP_SA_JSON_RAW)
logger.debug("GCP_SA_JSON: set")
else:
logger.debug("GCP_SA_JSON: not set")
except Exception as e:
logger.debug(f"GCP_SA_JSON: not set (invalid json): {e}")
GCP_SA_JSON_RAW = os.environ.get("GCP_SA_JSON")
GCP_SA_JSON = None
if GCP_SA_JSON_RAW:
try:
GCP_SA_JSON = json.loads(GCP_SA_JSON_RAW)
logger.debug("GCP_SA_JSON: set")
except json.JSONDecodeError as e:
raise ValueError("GCP_SA_JSON must be valid JSON") from e
elif TRANSCRIPTION_PROVIDER == "Dembrane-25-09":
raise AssertionError("GCP_SA_JSON environment variable is required for Dembrane-25-09")
else:
logger.debug("GCP_SA_JSON: not set")
🤖 Prompt for AI Agents
In echo/server/dembrane/config.py around lines 225-235, the code currently
swallows invalid or missing GCP service account JSON by logging at debug; change
it to validate and fail fast: if GCP_SA_JSON_RAW is present but json.loads
raises, log an error and raise a RuntimeError (or call sys.exit) with the parse
error so the process stops immediately; additionally, if the release/feature
flag for Dembrane-25-09 is active (check the existing config/flag used elsewhere
in this module) and GCP_SA_JSON_RAW is missing, log an error and raise similarly
so startup fails loudly rather than continuing until Vertex calls fail later.

Comment on lines +34 to +36
"created_at": {
"_gte": (get_utc_timestamp() - timedelta(minutes=5)).isoformat()
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix created_at filter direction.

The comment describes excluding conversations created in the last 5 minutes, but _gte on created_at keeps only those fresh rows. That reversal means we now nag brand-new conversations and still skip the stale ones we actually want. Flip it to _lte (or _lt) with the same timestamp.

-                    "created_at": {
-                        "_gte": (get_utc_timestamp() - timedelta(minutes=5)).isoformat()
-                    },
+                    "created_at": {
+                        "_lte": (get_utc_timestamp() - timedelta(minutes=5)).isoformat()
+                    },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"created_at": {
"_gte": (get_utc_timestamp() - timedelta(minutes=5)).isoformat()
},
"created_at": {
"_lte": (get_utc_timestamp() - timedelta(minutes=5)).isoformat()
},
🤖 Prompt for AI Agents
In echo/server/dembrane/conversation_utils.py around lines 34 to 36, the
created_at filter uses "_gte" which keeps only conversations from the last 5
minutes contrary to the intended behavior of excluding recent conversations;
change the comparator to "_lte" (or "_lt") using the same (get_utc_timestamp() -
timedelta(minutes=5)).isoformat() value so the query selects conversations
created at or before that cutoff (i.e., older than 5 minutes).

Comment on lines +125 to +154
new_project_data = {
"name": current_project["name"],
"language": current_project["language"],
"is_conversation_allowed": current_project["is_conversation_allowed"],
"directus_user_id": current_project["directus_user_id"],
"context": current_project["context"],
"default_conversation_title": current_project["default_conversation_title"],
"default_conversation_description": current_project["default_conversation_description"],
"default_conversation_finish_text": current_project["default_conversation_finish_text"],
"default_conversation_ask_for_participant_name": current_project[
"default_conversation_ask_for_participant_name"
],
"default_conversation_tutorial_slug": current_project[
"default_conversation_tutorial_slug"
],
"default_conversation_transcript_prompt": current_project[
"default_conversation_transcript_prompt"
],
"conversation_ask_for_participant_name_label": current_project[
"conversation_ask_for_participant_name_label"
],
"image_generation_model": current_project["image_generation_model"],
"is_enhanced_audio_processing_enabled": current_project[
"is_enhanced_audio_processing_enabled"
],
"is_get_reply_enabled": current_project["is_get_reply_enabled"],
"is_project_notification_subscription_allowed": current_project[
"is_project_notification_subscription_allowed"
],
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider extracting field list to a constant.

The 17-field list of project attributes to clone is embedded in the method. If this list needs to be maintained or reused elsewhere, consider extracting it to a module-level constant or a helper method.

This would make it easier to keep the cloned fields in sync with the project schema and improve maintainability.

🤖 Prompt for AI Agents
In echo/server/dembrane/service/project.py around lines 125 to 154, the
hard-coded 17-field mapping for cloning project attributes is embedded in the
method; extract that list of field names into a module-level constant (e.g.,
CLONABLE_PROJECT_FIELDS) or a small helper function that returns the dict by
iterating over that constant, then replace the inline mapping with a
comprehension or helper call that builds new_project_data from current_project
using the constant to improve reuse and maintainability.

#!/usr/bin/env bash

dramatiq --queues cpu --processes 2 --threads 1 dembrane.tasks
dramatiq --watch ./dembrane --queues cpu --processes 1 --threads 2 dembrane.tasks
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't break LightRAG's single-thread contract.
Threads=2 means two Dramatiq workers share a process and fight LightRAG’s global locks—we already learned these tasks must stay single-threaded. Keep the watch flag, but revert to threads=1 (bump processes instead if you need more throughput).

Apply this diff:

-dramatiq --watch ./dembrane --queues cpu --processes 1 --threads 2 dembrane.tasks
+dramatiq --watch ./dembrane --queues cpu --processes 1 --threads 1 dembrane.tasks

Based on learnings

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
dramatiq --watch ./dembrane --queues cpu --processes 1 --threads 2 dembrane.tasks
dramatiq --watch ./dembrane --queues cpu --processes 1 --threads 1 dembrane.tasks
🤖 Prompt for AI Agents
In echo/server/run-worker-cpu.sh around line 3 the Dramatiq invocation uses
--threads 2 which violates LightRAG's single-thread contract and causes global
lock contention; change the flag to --threads 1 (keep the --watch flag) and, if
more throughput is required, increase the --processes count instead of using
multiple threads.

spashii added a commit that referenced this pull request Nov 18, 2025
ECHO-xxx

- (feature) new "Dembrane 25-09" transcription provider used for all new
transcription (AssemblyAI + Gemini)
- (feature) updated retranscribe flow (pii redaction, toast that takes
you to new conversation)
- (feature) add clone project functionality (only metadata) + tests
- (bug) fix premature calling of "finish conversation" (duration bug)
- dev stuff:
    - use vertex ai instead of gemini api
    - remove grid view
- add biome for frontend linting and formatting (eslint + prettier was
breaking my pc lol)
    - refactor conversation transcript into maintainable components
- side effects:
    - remember to add GCP_SA_JSON env var. ask @spashii for it.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- New Features
  - Copy and download conversation transcripts (Markdown).
  - Retranscribe conversations with optional PII redaction.
  - Clone projects and export all transcripts as a ZIP.
  - View conversation status via a modal in Project settings.
- Improvements
- Streamlined transcript view with simpler controls and infinite scroll.
  - Unified Project settings sections (Upload, Export, Actions).
  - Projects page now uses a consistent list view.
- Documentation
  - Added operational notes and guidance.
- Chores
  - Switched editor tooling to Biome; updated workspace sessions.
- Tests
  - Added test for project shallow clone with tags.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working Feature improvement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant