karaoke-gen 0.75.53__py3-none-any.whl → 0.76.20__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. karaoke_gen/audio_fetcher.py +218 -0
  2. karaoke_gen/karaoke_gen.py +190 -25
  3. karaoke_gen/lyrics_processor.py +14 -25
  4. karaoke_gen/utils/__init__.py +26 -0
  5. karaoke_gen/utils/cli_args.py +9 -1
  6. karaoke_gen/utils/gen_cli.py +1 -1
  7. karaoke_gen/utils/remote_cli.py +33 -6
  8. {karaoke_gen-0.75.53.dist-info → karaoke_gen-0.76.20.dist-info}/METADATA +2 -2
  9. {karaoke_gen-0.75.53.dist-info → karaoke_gen-0.76.20.dist-info}/RECORD +36 -32
  10. lyrics_transcriber/frontend/index.html +5 -1
  11. lyrics_transcriber/frontend/package-lock.json +4553 -0
  12. lyrics_transcriber/frontend/package.json +3 -0
  13. lyrics_transcriber/frontend/playwright.config.ts +69 -0
  14. lyrics_transcriber/frontend/public/nomad-karaoke-logo.svg +5 -0
  15. lyrics_transcriber/frontend/src/App.tsx +88 -59
  16. lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +55 -21
  17. lyrics_transcriber/frontend/src/components/AppHeader.tsx +65 -0
  18. lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +5 -5
  19. lyrics_transcriber/frontend/src/components/DurationTimelineView.tsx +9 -9
  20. lyrics_transcriber/frontend/src/components/EditModal.tsx +1 -1
  21. lyrics_transcriber/frontend/src/components/EditWordList.tsx +1 -1
  22. lyrics_transcriber/frontend/src/components/Header.tsx +34 -48
  23. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +22 -21
  24. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +1 -1
  25. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +1 -1
  26. lyrics_transcriber/frontend/src/components/WordDivider.tsx +3 -3
  27. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +2 -2
  28. lyrics_transcriber/frontend/src/components/shared/constants.ts +15 -5
  29. lyrics_transcriber/frontend/src/main.tsx +1 -7
  30. lyrics_transcriber/frontend/src/theme.ts +337 -135
  31. lyrics_transcriber/frontend/vite.config.ts +5 -0
  32. lyrics_transcriber/frontend/yarn.lock +1005 -1046
  33. lyrics_transcriber/review/server.py +1 -1
  34. {karaoke_gen-0.75.53.dist-info → karaoke_gen-0.76.20.dist-info}/WHEEL +0 -0
  35. {karaoke_gen-0.75.53.dist-info → karaoke_gen-0.76.20.dist-info}/entry_points.txt +0 -0
  36. {karaoke_gen-0.75.53.dist-info → karaoke_gen-0.76.20.dist-info}/licenses/LICENSE +0 -0
@@ -19,6 +19,7 @@
19
19
  "@mui/icons-material": "^6.3.0",
20
20
  "@mui/material": "^6.3.0",
21
21
  "@mui/system": "^6.4.3",
22
+ "lucide-react": "^0.562.0",
22
23
  "nanoid": "^5.0.9",
23
24
  "react": "^18.3.1",
24
25
  "react-dom": "^18.3.1",
@@ -26,6 +27,7 @@
26
27
  },
27
28
  "devDependencies": {
28
29
  "@eslint/js": "^9.17.0",
30
+ "@playwright/test": "^1.57.0",
29
31
  "@types/react": "^18.3.18",
30
32
  "@types/react-dom": "^18.3.5",
31
33
  "@vitejs/plugin-react": "^4.3.4",
@@ -34,6 +36,7 @@
34
36
  "eslint-plugin-react-refresh": "^0.4.16",
35
37
  "gh-pages": "^6.3.0",
36
38
  "globals": "^15.14.0",
39
+ "playwright": "^1.57.0",
37
40
  "typescript": "~5.6.2",
38
41
  "typescript-eslint": "^8.18.2",
39
42
  "vite": "^6.0.5"
@@ -0,0 +1,69 @@
1
+ import { defineConfig, devices } from '@playwright/test';
2
+
3
+ /**
4
+ * Playwright configuration for lyrics-transcriber frontend E2E tests.
5
+ * Tests run against the local dev server (localhost:5173) served by vite.
6
+ */
7
+ export default defineConfig({
8
+ testDir: './e2e',
9
+
10
+ // Run tests sequentially for now
11
+ fullyParallel: false,
12
+
13
+ // Fail the build on CI if you accidentally left test.only in the source code
14
+ forbidOnly: !!process.env.CI,
15
+
16
+ // Retry on CI only
17
+ retries: process.env.CI ? 2 : 0,
18
+
19
+ // Single worker
20
+ workers: 1,
21
+
22
+ // Reporter to use
23
+ reporter: [
24
+ ['html', { open: 'never' }],
25
+ ['list'],
26
+ ],
27
+
28
+ // Shared settings for all tests
29
+ use: {
30
+ // Base URL for the local dev server
31
+ baseURL: 'http://localhost:5173',
32
+
33
+ // Collect trace on failure for debugging
34
+ trace: 'on-first-retry',
35
+
36
+ // Screenshot on failure
37
+ screenshot: 'only-on-failure',
38
+
39
+ // Video on failure
40
+ video: 'on-first-retry',
41
+
42
+ // Increase timeout for complex interactions
43
+ actionTimeout: 30000,
44
+ },
45
+
46
+ // Longer timeout for tests
47
+ timeout: 120000, // 2 minutes per test
48
+
49
+ // Expect timeout for assertions
50
+ expect: {
51
+ timeout: 30000, // 30 seconds for expects
52
+ },
53
+
54
+ // Configure projects (browsers)
55
+ projects: [
56
+ {
57
+ name: 'chromium',
58
+ use: { ...devices['Desktop Chrome'] },
59
+ },
60
+ ],
61
+
62
+ // Run local dev server before starting tests
63
+ webServer: {
64
+ command: 'yarn dev',
65
+ url: 'http://localhost:5173',
66
+ reuseExistingServer: !process.env.CI,
67
+ timeout: 120000,
68
+ },
69
+ });
@@ -0,0 +1,5 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3
+ <svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="45 60 255 135">
4
+ <g data-v-70b83f88="" fill="#ff7acc" class="basesvg" transform="translate(48.368003845214844,62.62239074707031)"><g fill-rule="" class="tp-name" transform="translate(0,0)"><g transform="scale(1.4000000000000004)"><g stroke="#ff7acc" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" paint-order="stroke" data-gra-attr="stroke" fill-opacity="0"><path d="M1.4-33.44L1.4 0C31.48 0 3.96 0 34.04 0L34.04-32.64 17.81-32.64 17.81-20.61 1.4-33.44ZM52.21-33.76C42.6-33.76 34.81-25.97 34.81-16.37 34.81-6.76 42.6 1.03 52.21 1.03 61.81 1.03 69.6-6.76 69.6-16.37 69.6-25.97 61.81-33.76 52.21-33.76ZM70.7 0L103.34 0 103.25-33.9 86.93-21.54 70.61-33.9 70.7 0ZM102.34 0C138.2 0 107.52 0 143.56 0L122.95-34.93 102.34 0ZM158.84 0.05C167.84 0.05 175.16-7.27 175.16-16.27 175.16-25.27 167.84-32.64 158.84-32.64L142.52-32.64C142.52 0.75 142.52-33.34 142.52 0.05L158.84 0.05Z" transform="translate(-1.399999976158142, 34.93000030517578)"></path></g><g transform="translate(0,38.959999084472656)"><g stroke="#ff7acc" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" paint-order="stroke" data-gra-attr="stroke" fill="#ff7acc" fill-opacity="0" transform="scale(1.02719)"><path d="M24.43 0L17.67 0 7.98-11.87 7.98 0 2.63 0 2.63-26.64 7.98-26.64 7.98-14.69 17.67-26.64 24.12-26.64 13.13-13.44 24.43 0ZM45.03 0L43.27-5.08 32.66-5.08 30.91 0 25.3 0 34.88-26.68 41.1-26.68 50.68 0 45.03 0ZM34.11-9.35L41.82-9.35 37.97-20.5 34.11-9.35ZM73.19 0L67.01 0 61.13-10.38 58.61-10.38 58.61 0 53.27 0 53.27-26.64 63.27-26.64Q66.36-26.64 68.53-25.55 70.71-24.47 71.8-22.61 72.89-20.76 72.89-18.47L72.89-18.47Q72.89-15.84 71.36-13.72 69.83-11.6 66.82-10.8L66.82-10.8 73.19 0ZM58.61-22.21L58.61-14.39 63.08-14.39Q65.25-14.39 66.32-15.44 67.39-16.49 67.39-18.36L67.39-18.36Q67.39-20.19 66.32-21.2 65.25-22.21 63.08-22.21L63.08-22.21 58.61-22.21ZM94.82 0L93.07-5.08 82.46-5.08 80.7 0 75.09 0 84.67-26.68 90.89-26.68 100.47 0 94.82 0ZM83.91-9.35L91.62-9.35 87.76-20.5 83.91-9.35ZM115.43 0.27Q111.69 0.27 108.56-1.49 105.43-3.24 103.6-6.35 101.76-9.47 101.76-13.4L101.76-13.4Q101.76-17.29 103.6-20.4 105.43-23.51 108.56-25.27 111.69-27.02 115.43-27.02L115.43-27.02Q119.21-27.02 122.32-25.27 125.43-23.51 127.24-20.4 129.05-17.29 129.05-13.4L129.05-13.4Q129.05-9.47 127.24-6.35 125.43-3.24 122.3-1.49 119.17 0.27 115.43 0.27L115.43 0.27ZM115.43-4.5Q117.83-4.5 119.66-5.59 121.5-6.68 122.53-8.7 123.56-10.73 123.56-13.4L123.56-13.4Q123.56-16.07 122.53-18.07 121.5-20.08 119.66-21.15 117.83-22.21 115.43-22.21L115.43-22.21Q113.02-22.21 111.17-21.15 109.32-20.08 108.29-18.07 107.26-16.07 107.26-13.4L107.26-13.4Q107.26-10.73 108.29-8.7 109.32-6.68 111.17-5.59 113.02-4.5 115.43-4.5L115.43-4.5ZM153.82 0L147.06 0 137.37-11.87 137.37 0 132.02 0 132.02-26.64 137.37-26.64 137.37-14.69 147.06-26.64 153.51-26.64 142.52-13.44 153.82 0ZM171.79-22.33L161.67-22.33 161.67-15.65 170.64-15.65 170.64-11.41 161.67-11.41 161.67-4.35 171.79-4.35 171.79 0 156.33 0 156.33-26.68 171.79-26.68 171.79-22.33Z" transform="translate(-2.630000114440918, 27.020000457763672)"></path></g></g></g></g> <g data-gra="path-slogan" fill-rule="" class="tp-slogan" fill="#ffdf6b" transform="translate(5,111.788818359375)"><rect x="0" height="1" y="5.9832000732421875" width="14.278396606445312"></rect> <rect height="1" y="5.9832000732421875" width="14.278396606445312" x="218.985595703125"></rect> <g transform="translate(17.278396606445312,0)"><g transform="scale(1.2800000000000002)"><path d="M10.21-8.38L12.02-8.38L9.68 0L7.70 0L6.13-5.96L4.49 0L2.52 0.01L0.26-8.38L2.06-8.38L3.54-1.87L5.24-8.38L7.12-8.38L8.72-1.91L10.21-8.38ZM18.38-8.38L20.06-8.38L20.06 0L18.38 0L18.38-3.56L14.80-3.56L14.80 0L13.12 0L13.12-8.38L14.80-8.38L14.80-4.93L18.38-4.93L18.38-8.38ZM26.58-7.02L23.40-7.02L23.40-4.92L26.22-4.92L26.22-3.59L23.40-3.59L23.40-1.37L26.58-1.37L26.58 0L21.72 0L21.72-8.39L26.58-8.39L26.58-7.02ZM34.37 0L32.42 0L30.58-3.26L29.78-3.26L29.78 0L28.10 0L28.10-8.38L31.25-8.38Q32.22-8.38 32.90-8.03Q33.59-7.69 33.93-7.11Q34.27-6.53 34.27-5.81L34.27-5.81Q34.27-4.98 33.79-4.31Q33.31-3.65 32.36-3.40L32.36-3.40L34.37 0ZM29.78-6.98L29.78-4.52L31.19-4.52Q31.87-4.52 32.21-4.85Q32.54-5.18 32.54-5.77L32.54-5.77Q32.54-6.35 32.21-6.67Q31.87-6.98 31.19-6.98L31.19-6.98L29.78-6.98ZM40.66-7.02L37.48-7.02L37.48-4.92L40.30-4.92L40.30-3.59L37.48-3.59L37.48-1.37L40.66-1.37L40.66 0L35.80 0L35.80-8.39L40.66-8.39L40.66-7.02ZM47.92-8.38L49.70-8.38L46.63 0L44.59 0L41.52-8.38L43.32-8.38L45.62-1.72L47.92-8.38ZM55.57-7.02L52.39-7.02L52.39-4.92L55.21-4.92L55.21-3.59L52.39-3.59L52.39-1.37L55.57-1.37L55.57 0L50.71 0L50.71-8.39L55.57-8.39L55.57-7.02ZM63.36 0L61.42 0L59.57-3.26L58.78-3.26L58.78 0L57.10 0L57.10-8.38L60.24-8.38Q61.21-8.38 61.90-8.03Q62.58-7.69 62.92-7.11Q63.26-6.53 63.26-5.81L63.26-5.81Q63.26-4.98 62.78-4.31Q62.30-3.65 61.36-3.40L61.36-3.40L63.36 0ZM58.78-6.98L58.78-4.52L60.18-4.52Q60.86-4.52 61.20-4.85Q61.54-5.18 61.54-5.77L61.54-5.77Q61.54-6.35 61.20-6.67Q60.86-6.98 60.18-6.98L60.18-6.98L58.78-6.98ZM72.43-8.38L74.30-8.38L71.47-2.92L71.47 0L69.79 0L69.79-2.92L66.95-8.38L68.84-8.38L70.64-4.55L72.43-8.38ZM79.16 0.08Q77.99 0.08 77.00-0.47Q76.02-1.02 75.44-2.00Q74.87-2.98 74.87-4.21L74.87-4.21Q74.87-5.44 75.44-6.41Q76.02-7.39 77.00-7.94Q77.99-8.50 79.16-8.50L79.16-8.50Q80.35-8.50 81.33-7.94Q82.31-7.39 82.88-6.41Q83.45-5.44 83.45-4.21L83.45-4.21Q83.45-2.98 82.88-2.00Q82.31-1.02 81.32-0.47Q80.34 0.08 79.16 0.08L79.16 0.08ZM79.16-1.42Q79.92-1.42 80.50-1.76Q81.07-2.10 81.40-2.74Q81.72-3.37 81.72-4.21L81.72-4.21Q81.72-5.05 81.40-5.68Q81.07-6.31 80.50-6.65Q79.92-6.98 79.16-6.98L79.16-6.98Q78.41-6.98 77.83-6.65Q77.24-6.31 76.92-5.68Q76.60-5.05 76.60-4.21L76.60-4.21Q76.60-3.37 76.92-2.74Q77.24-2.10 77.83-1.76Q78.41-1.42 79.16-1.42L79.16-1.42ZM84.67-8.38L86.35-8.38L86.35-3.19Q86.35-2.34 86.80-1.89Q87.24-1.44 88.04-1.44L88.04-1.44Q88.86-1.44 89.30-1.89Q89.75-2.34 89.75-3.19L89.75-3.19L89.75-8.38L91.44-8.38L91.44-3.20Q91.44-2.14 90.98-1.40Q90.52-0.66 89.74-0.29Q88.97 0.08 88.02 0.08L88.02 0.08Q87.08 0.08 86.32-0.29Q85.56-0.66 85.12-1.40Q84.67-2.14 84.67-3.20L84.67-3.20L84.67-8.38ZM101.60 0L101.05-1.60L97.72-1.60L97.16 0L95.40 0L98.41-8.39L100.37-8.39L103.38 0L101.60 0ZM98.17-2.94L100.60-2.94L99.38-6.44L98.17-2.94ZM110.77 0L108.83 0L106.98-3.26L106.19-3.26L106.19 0L104.51 0L104.51-8.38L107.65-8.38Q108.62-8.38 109.31-8.03Q109.99-7.69 110.33-7.11Q110.68-6.53 110.68-5.81L110.68-5.81Q110.68-4.98 110.20-4.31Q109.72-3.65 108.77-3.40L108.77-3.40L110.77 0ZM106.19-6.98L106.19-4.52L107.59-4.52Q108.28-4.52 108.61-4.85Q108.95-5.18 108.95-5.77L108.95-5.77Q108.95-6.35 108.61-6.67Q108.28-6.98 107.59-6.98L107.59-6.98L106.19-6.98ZM117.06-7.02L113.88-7.02L113.88-4.92L116.70-4.92L116.70-3.59L113.88-3.59L113.88-1.37L117.06-1.37L117.06 0L112.20 0L112.20-8.39L117.06-8.39L117.06-7.02ZM118.72-1.76L120.42-1.76L119.02 1.61L117.94 1.61L118.72-1.76ZM127.38 0.08Q126.50 0.08 125.80-0.22Q125.10-0.52 124.69-1.08Q124.28-1.64 124.27-2.41L124.27-2.41L126.07-2.41Q126.11-1.90 126.44-1.60Q126.77-1.30 127.34-1.30L127.34-1.30Q127.93-1.30 128.27-1.58Q128.60-1.86 128.60-2.32L128.60-2.32Q128.60-2.69 128.38-2.93Q128.15-3.17 127.81-3.31Q127.46-3.44 126.86-3.61L126.86-3.61Q126.05-3.85 125.54-4.09Q125.03-4.32 124.66-4.79Q124.30-5.27 124.30-6.06L124.30-6.06Q124.30-6.80 124.67-7.36Q125.04-7.91 125.71-8.20Q126.38-8.50 127.25-8.50L127.25-8.50Q128.54-8.50 129.35-7.87Q130.16-7.24 130.25-6.11L130.25-6.11L128.40-6.11Q128.38-6.54 128.03-6.82Q127.69-7.10 127.13-7.10L127.13-7.10Q126.64-7.10 126.34-6.85Q126.05-6.60 126.05-6.12L126.05-6.12Q126.05-5.78 126.27-5.56Q126.49-5.34 126.82-5.20Q127.15-5.06 127.75-4.88L127.75-4.88Q128.57-4.64 129.08-4.40Q129.60-4.16 129.97-3.68Q130.34-3.20 130.34-2.42L130.34-2.42Q130.34-1.75 130.00-1.18Q129.65-0.60 128.98-0.26Q128.30 0.08 127.38 0.08L127.38 0.08ZM131.80-8.38L133.48-8.38L133.48 0L131.80 0L131.80-8.38ZM142.30-8.39L142.30 0L140.62 0L136.81-5.75L136.81 0L135.13 0L135.13-8.39L136.81-8.39L140.62-2.63L140.62-8.39L142.30-8.39ZM151.72-5.86L149.78-5.86Q149.50-6.38 148.99-6.66Q148.49-6.94 147.82-6.94L147.82-6.94Q147.07-6.94 146.50-6.60Q145.92-6.26 145.60-5.64Q145.27-5.02 145.27-4.20L145.27-4.20Q145.27-3.36 145.60-2.74Q145.93-2.11 146.52-1.78Q147.11-1.44 147.89-1.44L147.89-1.44Q148.85-1.44 149.46-1.95Q150.07-2.46 150.26-3.37L150.26-3.37L147.38-3.37L147.38-4.66L151.92-4.66L151.92-3.19Q151.75-2.32 151.20-1.57Q150.65-0.83 149.78-0.38Q148.91 0.07 147.83 0.07L147.83 0.07Q146.62 0.07 145.64-0.47Q144.66-1.02 144.10-1.99Q143.54-2.96 143.54-4.20L143.54-4.20Q143.54-5.44 144.10-6.41Q144.66-7.39 145.64-7.94Q146.62-8.48 147.82-8.48L147.82-8.48Q149.23-8.48 150.28-7.79Q151.32-7.10 151.72-5.86L151.72-5.86ZM153.53-8.52L155.34-8.52L155.15-2.72L153.73-2.72L153.53-8.52ZM154.48 0.08Q154.02 0.08 153.73-0.20Q153.43-0.48 153.43-0.90L153.43-0.90Q153.43-1.32 153.73-1.60Q154.02-1.88 154.48-1.88L154.48-1.88Q154.92-1.88 155.21-1.60Q155.50-1.32 155.50-0.90L155.50-0.90Q155.50-0.48 155.21-0.20Q154.92 0.08 154.48 0.08L154.48 0.08Z" transform="translate(-0.264, 8.52)"></path></g></g></g></g>
5
+ </svg>
@@ -1,12 +1,23 @@
1
1
  import UploadFileIcon from '@mui/icons-material/UploadFile'
2
2
  import { Alert, Box, Button, Modal, Typography } from '@mui/material'
3
- import { useEffect, useState } from 'react'
3
+ import { ThemeProvider } from '@mui/material/styles'
4
+ import CssBaseline from '@mui/material/CssBaseline'
5
+ import { useEffect, useState, useMemo } from 'react'
4
6
  import { ApiClient, FileOnlyClient, LiveApiClient } from './api'
5
7
  import CorrectionMetrics from './components/CorrectionMetrics'
6
8
  import LyricsAnalyzer from './components/LyricsAnalyzer'
9
+ import AppHeader from './components/AppHeader'
7
10
  import { CorrectionData } from './types'
11
+ import { createAppTheme } from './theme'
12
+
13
+ const THEME_STORAGE_KEY = 'nomad-karaoke-lyrics-theme'
8
14
 
9
15
  export default function App() {
16
+ const [isDarkMode, setIsDarkMode] = useState(() => {
17
+ // Initialize from localStorage
18
+ const stored = localStorage.getItem(THEME_STORAGE_KEY)
19
+ return stored !== 'light' // Default to dark
20
+ })
10
21
  const [data, setData] = useState<CorrectionData | null>(null)
11
22
  const [showMetadata, setShowMetadata] = useState(false)
12
23
  const [error, setError] = useState<string | null>(null)
@@ -14,6 +25,16 @@ export default function App() {
14
25
  const [isReadOnly, setIsReadOnly] = useState(true)
15
26
  const [audioHash, setAudioHash] = useState<string>('')
16
27
 
28
+ // Create theme based on mode
29
+ const theme = useMemo(() => createAppTheme(isDarkMode ? 'dark' : 'light'), [isDarkMode])
30
+
31
+ // Handle theme toggle
32
+ const handleToggleTheme = () => {
33
+ const newIsDark = !isDarkMode
34
+ setIsDarkMode(newIsDark)
35
+ localStorage.setItem(THEME_STORAGE_KEY, newIsDark ? 'dark' : 'light')
36
+ }
37
+
17
38
  useEffect(() => {
18
39
  // Parse query parameters
19
40
  const params = new URLSearchParams(window.location.search)
@@ -145,70 +166,78 @@ export default function App() {
145
166
 
146
167
  if (!data) {
147
168
  return (
148
- <Box sx={{ p: 3 }}>
149
- {error && (
150
- <Alert severity="error" sx={{ mb: 2 }} onClose={() => setError(null)}>
151
- {error}
152
- </Alert>
153
- )}
154
- {isReadOnly ? (
155
- <>
156
- <Alert severity="info" sx={{ mb: 2 }}>
157
- Running in read-only mode. Connect to an API to enable editing.
169
+ <ThemeProvider theme={theme}>
170
+ <CssBaseline />
171
+ <AppHeader isDarkMode={isDarkMode} onToggleTheme={handleToggleTheme} />
172
+ <Box sx={{ p: 3 }}>
173
+ {error && (
174
+ <Alert severity="error" sx={{ mb: 2 }} onClose={() => setError(null)}>
175
+ {error}
158
176
  </Alert>
159
- <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
160
- <Typography variant="h4">
161
- Lyrics Correction Review
177
+ )}
178
+ {isReadOnly ? (
179
+ <>
180
+ <Alert severity="info" sx={{ mb: 2 }}>
181
+ Running in read-only mode. Connect to an API to enable editing.
182
+ </Alert>
183
+ <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
184
+ <Typography variant="h4">
185
+ Lyrics Correction Review
186
+ </Typography>
187
+ <Button
188
+ variant="outlined"
189
+ startIcon={<UploadFileIcon />}
190
+ onClick={handleFileLoad}
191
+ >
192
+ Load File
193
+ </Button>
194
+ </Box>
195
+ <Box sx={{ mb: 3 }}>
196
+ <CorrectionMetrics />
197
+ </Box>
198
+ </>
199
+ ) : (
200
+ <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '50vh' }}>
201
+ <Typography variant="h6" color="text.secondary">
202
+ Loading Lyrics Transcription Review...
162
203
  </Typography>
163
- <Button
164
- variant="outlined"
165
- startIcon={<UploadFileIcon />}
166
- onClick={handleFileLoad}
167
- >
168
- Load File
169
- </Button>
170
- </Box>
171
- <Box sx={{ mb: 3 }}>
172
- <CorrectionMetrics />
173
204
  </Box>
174
- </>
175
- ) : (
176
- <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '50vh' }}>
177
- <Typography variant="h6" color="text.secondary">
178
- Loading Lyrics Correction Review...
179
- </Typography>
180
- </Box>
181
- )}
182
- </Box>
205
+ )}
206
+ </Box>
207
+ </ThemeProvider>
183
208
  )
184
209
  }
185
210
 
186
211
  return (
187
- <Box sx={{
188
- p: 1.5,
189
- pb: 3,
190
- maxWidth: '100%',
191
- overflowX: 'hidden'
192
- }}>
193
- {error && (
194
- <Alert severity="error" sx={{ mb: 1 }} onClose={() => setError(null)}>
195
- {error}
196
- </Alert>
197
- )}
198
- {isReadOnly && (
199
- <Alert severity="info" sx={{ mb: 1 }}>
200
- Running in read-only mode. Connect to an API to enable editing.
201
- </Alert>
202
- )}
203
- <LyricsAnalyzer
204
- data={data}
205
- onFileLoad={handleFileLoad}
206
- onShowMetadata={() => setShowMetadata(true)}
207
- apiClient={apiClient}
208
- isReadOnly={isReadOnly}
209
- audioHash={audioHash}
210
- />
211
- {renderMetadataModal()}
212
- </Box>
212
+ <ThemeProvider theme={theme}>
213
+ <CssBaseline />
214
+ <AppHeader isDarkMode={isDarkMode} onToggleTheme={handleToggleTheme} />
215
+ <Box sx={{
216
+ p: 1.5,
217
+ pb: 3,
218
+ maxWidth: '100%',
219
+ overflowX: 'hidden'
220
+ }}>
221
+ {error && (
222
+ <Alert severity="error" sx={{ mb: 1 }} onClose={() => setError(null)}>
223
+ {error}
224
+ </Alert>
225
+ )}
226
+ {isReadOnly && (
227
+ <Alert severity="info" sx={{ mb: 1 }}>
228
+ Running in read-only mode. Connect to an API to enable editing.
229
+ </Alert>
230
+ )}
231
+ <LyricsAnalyzer
232
+ data={data}
233
+ onFileLoad={handleFileLoad}
234
+ onShowMetadata={() => setShowMetadata(true)}
235
+ apiClient={apiClient}
236
+ isReadOnly={isReadOnly}
237
+ audioHash={audioHash}
238
+ />
239
+ {renderMetadataModal()}
240
+ </Box>
241
+ </ThemeProvider>
213
242
  )
214
243
  }
@@ -1,4 +1,4 @@
1
- import React from "react";
1
+ import React, { useEffect, useRef } from "react";
2
2
 
3
3
  type Props = {
4
4
  isOpen: boolean;
@@ -12,22 +12,57 @@ export const AIFeedbackModal: React.FC<Props> = ({ isOpen, onClose, onSubmit, su
12
12
  const [finalText, setFinalText] = React.useState("");
13
13
  const [reasonCategory, setReason] = React.useState("AI_CORRECT");
14
14
  const [reasonDetail, setDetail] = React.useState("");
15
+ const modalRef = useRef<HTMLDivElement>(null);
16
+
17
+ // Handle Escape key to close modal
18
+ useEffect(() => {
19
+ const handleKeyDown = (e: KeyboardEvent) => {
20
+ if (e.key === 'Escape' && isOpen) {
21
+ onClose();
22
+ }
23
+ };
24
+ document.addEventListener('keydown', handleKeyDown);
25
+ return () => document.removeEventListener('keydown', handleKeyDown);
26
+ }, [isOpen, onClose]);
27
+
28
+ // Focus modal on open
29
+ useEffect(() => {
30
+ if (isOpen && modalRef.current) {
31
+ modalRef.current.focus();
32
+ }
33
+ }, [isOpen]);
15
34
 
16
35
  if (!isOpen) return null;
17
36
 
37
+ // Dark theme colors matching karaoke-gen
38
+ const colors = {
39
+ background: '#1a1a1a', // slate-800
40
+ text: '#f8fafc', // slate-50
41
+ textSecondary: '#888888', // slate-400
42
+ border: '#2a2a2a', // slate-700
43
+ inputBg: '#0f0f0f', // slate-900
44
+ };
45
+
18
46
  return (
19
- <div style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.5)", display: "flex", alignItems: "center", justifyContent: "center" }}>
20
- <div style={{ background: "#fff", padding: 16, width: 480, borderRadius: 8 }}>
21
- <h3>AI Suggestion</h3>
22
- <p style={{ marginTop: 8 }}>
47
+ <div style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.7)", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1300 }}>
48
+ <div
49
+ ref={modalRef}
50
+ role="dialog"
51
+ aria-modal="true"
52
+ aria-labelledby="ai-feedback-title"
53
+ tabIndex={-1}
54
+ style={{ background: colors.background, padding: 16, width: 480, borderRadius: 8, border: `1px solid ${colors.border}`, color: colors.text, outline: 'none' }}
55
+ >
56
+ <h3 id="ai-feedback-title" style={{ color: colors.text, margin: 0 }}>AI Suggestion</h3>
57
+ <p style={{ marginTop: 8, color: colors.text }}>
23
58
  {suggestion?.text ?? "No suggestion"}
24
59
  {suggestion?.confidence != null ? ` (confidence ${Math.round((suggestion.confidence || 0) * 100)}%)` : null}
25
60
  </p>
26
- {suggestion?.reasoning ? <small>{suggestion.reasoning}</small> : null}
61
+ {suggestion?.reasoning ? <small style={{ color: colors.textSecondary }}>{suggestion.reasoning}</small> : null}
27
62
 
28
- <div style={{ marginTop: 12 }}>
29
- <label>Action</label>
30
- <select value={reviewerAction} onChange={(e) => setAction(e.target.value)} style={{ marginLeft: 8 }}>
63
+ <div style={{ marginTop: 12, display: 'flex', alignItems: 'center', gap: 8 }}>
64
+ <label htmlFor="ai-action-select" style={{ color: colors.text }}>Action</label>
65
+ <select id="ai-action-select" value={reviewerAction} onChange={(e) => setAction(e.target.value)} style={{ background: colors.inputBg, color: colors.text, border: `1px solid ${colors.border}`, borderRadius: 4, padding: '4px 8px' }}>
31
66
  <option value="ACCEPT">Accept</option>
32
67
  <option value="REJECT">Reject</option>
33
68
  <option value="MODIFY">Modify</option>
@@ -35,15 +70,15 @@ export const AIFeedbackModal: React.FC<Props> = ({ isOpen, onClose, onSubmit, su
35
70
  </div>
36
71
 
37
72
  {reviewerAction === "MODIFY" ? (
38
- <div style={{ marginTop: 12 }}>
39
- <label>Final Text</label>
40
- <input value={finalText} onChange={(e) => setFinalText(e.target.value)} style={{ marginLeft: 8, width: "100%" }} />
73
+ <div style={{ marginTop: 12, display: 'flex', flexDirection: 'column', gap: 4 }}>
74
+ <label htmlFor="ai-final-text" style={{ color: colors.text }}>Final Text</label>
75
+ <input id="ai-final-text" value={finalText} onChange={(e) => setFinalText(e.target.value)} style={{ width: "100%", background: colors.inputBg, color: colors.text, border: `1px solid ${colors.border}`, borderRadius: 4, padding: '4px 8px', boxSizing: 'border-box' }} />
41
76
  </div>
42
77
  ) : null}
43
78
 
44
- <div style={{ marginTop: 12 }}>
45
- <label>Reason</label>
46
- <select value={reasonCategory} onChange={(e) => setReason(e.target.value)} style={{ marginLeft: 8 }}>
79
+ <div style={{ marginTop: 12, display: 'flex', alignItems: 'center', gap: 8 }}>
80
+ <label htmlFor="ai-reason-select" style={{ color: colors.text }}>Reason</label>
81
+ <select id="ai-reason-select" value={reasonCategory} onChange={(e) => setReason(e.target.value)} style={{ background: colors.inputBg, color: colors.text, border: `1px solid ${colors.border}`, borderRadius: 4, padding: '4px 8px' }}>
47
82
  <option value="AI_CORRECT">AI_CORRECT</option>
48
83
  <option value="AI_INCORRECT">AI_INCORRECT</option>
49
84
  <option value="AI_SUBOPTIMAL">AI_SUBOPTIMAL</option>
@@ -52,17 +87,18 @@ export const AIFeedbackModal: React.FC<Props> = ({ isOpen, onClose, onSubmit, su
52
87
  </select>
53
88
  </div>
54
89
 
55
- <div style={{ marginTop: 12 }}>
56
- <label>Details</label>
57
- <textarea value={reasonDetail} onChange={(e) => setDetail(e.target.value)} style={{ marginLeft: 8, width: "100%" }} />
90
+ <div style={{ marginTop: 12, display: 'flex', flexDirection: 'column', gap: 4 }}>
91
+ <label htmlFor="ai-details-textarea" style={{ color: colors.text }}>Details</label>
92
+ <textarea id="ai-details-textarea" value={reasonDetail} onChange={(e) => setDetail(e.target.value)} style={{ width: "100%", background: colors.inputBg, color: colors.text, border: `1px solid ${colors.border}`, borderRadius: 4, padding: '4px 8px', boxSizing: 'border-box' }} />
58
93
  </div>
59
94
 
60
95
  <div style={{ display: "flex", gap: 8, justifyContent: "flex-end", marginTop: 16 }}>
61
- <button onClick={onClose}>Cancel</button>
96
+ <button onClick={onClose} style={{ background: colors.border, color: colors.text, border: 'none', borderRadius: 4, padding: '6px 12px', cursor: 'pointer' }}>Cancel</button>
62
97
  <button
63
98
  onClick={() =>
64
99
  onSubmit({ reviewerAction, finalText: finalText || undefined, reasonCategory, reasonDetail: reasonDetail || undefined })
65
100
  }
101
+ style={{ background: '#f97316', color: '#fff', border: 'none', borderRadius: 4, padding: '6px 12px', cursor: 'pointer' }}
66
102
  >
67
103
  Submit
68
104
  </button>
@@ -73,5 +109,3 @@ export const AIFeedbackModal: React.FC<Props> = ({ isOpen, onClose, onSubmit, su
73
109
  };
74
110
 
75
111
  export default AIFeedbackModal;
76
-
77
-
@@ -0,0 +1,65 @@
1
+ import { Box, IconButton, Tooltip, Typography, useTheme } from '@mui/material'
2
+ import { Sun, Moon } from 'lucide-react'
3
+
4
+ interface AppHeaderProps {
5
+ isDarkMode?: boolean
6
+ onToggleTheme?: () => void
7
+ }
8
+
9
+ export default function AppHeader({ isDarkMode = true, onToggleTheme }: AppHeaderProps) {
10
+ const theme = useTheme()
11
+
12
+ return (
13
+ <Box
14
+ component="header"
15
+ sx={{
16
+ borderBottom: `1px solid ${theme.palette.divider}`,
17
+ backgroundColor: theme.palette.background.paper,
18
+ backdropFilter: 'blur(8px)',
19
+ position: 'sticky',
20
+ top: 0,
21
+ zIndex: 1100,
22
+ px: 2,
23
+ py: 1.5,
24
+ display: 'flex',
25
+ alignItems: 'center',
26
+ justifyContent: 'space-between',
27
+ }}
28
+ >
29
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
30
+ <img src="/nomad-karaoke-logo.svg" alt="Nomad Karaoke" style={{ height: 40 }} />
31
+ <Typography
32
+ variant="h6"
33
+ sx={{
34
+ fontWeight: 'bold',
35
+ color: theme.palette.text.primary,
36
+ fontSize: '1.1rem',
37
+ lineHeight: 1,
38
+ m: 0,
39
+ }}
40
+ >
41
+ Lyrics Transcription Review
42
+ </Typography>
43
+ </Box>
44
+
45
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
46
+ {onToggleTheme && (
47
+ <Tooltip title={isDarkMode ? 'Switch to light mode' : 'Switch to dark mode'}>
48
+ <IconButton
49
+ onClick={onToggleTheme}
50
+ sx={{
51
+ color: theme.palette.text.secondary,
52
+ '&:hover': {
53
+ color: theme.palette.text.primary,
54
+ backgroundColor: theme.palette.action.hover,
55
+ },
56
+ }}
57
+ >
58
+ {isDarkMode ? <Sun className="w-5 h-5" /> : <Moon className="w-5 h-5" />}
59
+ </IconButton>
60
+ </Tooltip>
61
+ )}
62
+ </Box>
63
+ </Box>
64
+ )
65
+ }
@@ -42,7 +42,7 @@ const WordContainer = styled(Box, {
42
42
  '50%': { opacity: 0.5 }
43
43
  },
44
44
  '&:hover': {
45
- backgroundColor: '#c8e6c9'
45
+ backgroundColor: 'rgba(34, 197, 94, 0.35)' // green tint hover for dark mode
46
46
  }
47
47
  }))
48
48
 
@@ -51,7 +51,7 @@ const OriginalWordLabel = styled(Box)({
51
51
  top: '-14px',
52
52
  left: '0',
53
53
  fontSize: '0.6rem',
54
- color: '#666',
54
+ color: '#888888', // slate-400 for dark mode
55
55
  textDecoration: 'line-through',
56
56
  opacity: 0.7,
57
57
  whiteSpace: 'nowrap',
@@ -71,10 +71,10 @@ const ActionButton = styled(IconButton)(({ theme }) => ({
71
71
  minHeight: '20px',
72
72
  width: '20px',
73
73
  height: '20px',
74
- backgroundColor: 'rgba(255, 255, 255, 0.9)',
75
- border: '1px solid rgba(0, 0, 0, 0.1)',
74
+ backgroundColor: 'rgba(30, 41, 59, 0.9)', // slate-800 with opacity for dark mode
75
+ border: '1px solid rgba(248, 250, 252, 0.1)', // light border for dark mode
76
76
  '&:hover': {
77
- backgroundColor: 'rgba(255, 255, 255, 1)',
77
+ backgroundColor: 'rgba(51, 65, 85, 1)', // slate-700 for dark mode
78
78
  transform: 'scale(1.1)'
79
79
  },
80
80
  '& .MuiSvgIcon-root': {
@@ -30,7 +30,7 @@ const SegmentTimeline = styled(Box)({
30
30
  const TimelineRuler = styled(Box)({
31
31
  position: 'relative',
32
32
  height: '20px',
33
- borderBottom: '1px solid #ccc',
33
+ borderBottom: '1px solid #2a2a2a', // slate-700 for dark mode
34
34
  marginBottom: '4px'
35
35
  })
36
36
 
@@ -38,14 +38,14 @@ const TimelineMark = styled(Box)({
38
38
  position: 'absolute',
39
39
  width: '1px',
40
40
  height: '8px',
41
- backgroundColor: '#999',
41
+ backgroundColor: '#666666', // slate-500 for dark mode
42
42
  bottom: 0
43
43
  })
44
44
 
45
45
  const TimelineLabel = styled(Typography)({
46
46
  position: 'absolute',
47
47
  fontSize: '0.65rem',
48
- color: '#666',
48
+ color: '#888888', // slate-400 for dark mode
49
49
  bottom: '10px',
50
50
  transform: 'translateX(-50%)',
51
51
  whiteSpace: 'nowrap'
@@ -58,7 +58,7 @@ const WordsBar = styled(Box)({
58
58
  alignItems: 'stretch',
59
59
  minWidth: '100%',
60
60
  touchAction: 'pan-y', // Better mobile scrolling
61
- backgroundColor: '#f5f5f5',
61
+ backgroundColor: '#0f0f0f', // slate-900 for dark mode
62
62
  borderRadius: '4px',
63
63
  marginBottom: '8px'
64
64
  })
@@ -84,7 +84,7 @@ const WordBar = styled(Box, {
84
84
  ? COLORS.corrected
85
85
  : isGap
86
86
  ? COLORS.uncorrectedGap
87
- : '#e0e0e0',
87
+ : '#2a2a2a', // slate-700 for dark mode default
88
88
  border: isLong ? '2px solid #f44336' : 'none',
89
89
  boxShadow: isLong ? '0 0 4px rgba(244, 67, 54, 0.5)' : 'none',
90
90
  '&:hover': {
@@ -101,13 +101,13 @@ const WordBar = styled(Box, {
101
101
 
102
102
  const OriginalWordLabel = styled(Typography)({
103
103
  fontSize: '0.65rem',
104
- color: '#888',
104
+ color: '#888888', // slate-400 for dark mode
105
105
  lineHeight: 1.1,
106
106
  marginBottom: '3px',
107
107
  textDecoration: 'line-through',
108
108
  opacity: 0.85,
109
109
  fontWeight: 500,
110
- backgroundColor: 'rgba(255, 255, 255, 0.8)',
110
+ backgroundColor: 'rgba(15, 23, 42, 0.8)', // slate-900 with opacity for dark mode
111
111
  padding: '1px 3px',
112
112
  borderRadius: '2px'
113
113
  })
@@ -226,7 +226,7 @@ export default function DurationTimelineView({
226
226
  whiteSpace: 'nowrap',
227
227
  width: '100%',
228
228
  textAlign: 'center',
229
- color: correction ? '#1b5e20' : 'inherit'
229
+ color: correction ? '#4ade80' : '#e5e5e5' // green-400 or slate-50 for dark mode
230
230
  }}
231
231
  >
232
232
  {word.text}
@@ -235,7 +235,7 @@ export default function DurationTimelineView({
235
235
  <Typography
236
236
  sx={{
237
237
  fontSize: '0.6rem',
238
- color: 'rgba(0,0,0,0.6)',
238
+ color: 'rgba(248, 250, 252, 0.6)', // slate-50 with opacity for dark mode
239
239
  lineHeight: 1,
240
240
  marginTop: '3px',
241
241
  fontWeight: 600
@@ -624,7 +624,7 @@ export default function EditModal({
624
624
  position: 'absolute',
625
625
  top: 0,
626
626
  left: 0,
627
- backgroundColor: 'rgba(255, 255, 255, 0.9)',
627
+ backgroundColor: 'rgba(30, 41, 59, 0.95)', // slate-800 with opacity for dark mode
628
628
  zIndex: 10
629
629
  }}>
630
630
  <CircularProgress size={60} thickness={4} />
@@ -312,7 +312,7 @@ export default function EditWordList({
312
312
  width: '8px',
313
313
  },
314
314
  '&::-webkit-scrollbar-thumb': {
315
- backgroundColor: 'rgba(0,0,0,0.2)',
315
+ backgroundColor: 'rgba(248, 250, 252, 0.2)', // slate-50 for dark mode
316
316
  borderRadius: '4px',
317
317
  },
318
318
  scrollbarWidth: 'thin',