Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .claude/hooks/session-start.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/bin/bash
set -e

echo "=== AudioSpec_Analyzer Session Start ==="
echo "Node: $(node --version)"
echo "npm: $(npm --version)"

if [ -f "package.json" ]; then
echo "Installing dependencies..."
npm install --silent
echo "Dependencies ready."
fi

if [ ! -f ".env" ]; then
echo "WARNING: .env not found. Copy .env.example to .env and set GEMINI_API_KEY."
fi

echo "=== Ready ==="
15 changes: 15 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"hooks": {
"SessionStart": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/session-start.sh"
}
]
}
]
}
}
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Gemini API Key (required for AI Transcribe / Summarize features)
# Get your key at: https://aistudio.google.com/app/apikey
# SECURITY: Never commit the real key. This file is safe to commit.
GEMINI_API_KEY=your_gemini_api_key_here
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: CI

on:
push:
branches:
- main
- 'claude/**'
pull_request:
branches:
- main

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Install dependencies
run: npm install

- name: Build
run: npm run build
env:
GEMINI_API_KEY: placeholder_for_ci_build
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ dist
dist-ssr
*.local

# Environment variables — never commit secrets
.env
.env.*
!.env.example

# Editor directories and files
.vscode/*
!.vscode/extensions.json
Expand Down
23 changes: 18 additions & 5 deletions App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import { TrackItem } from './components/TrackItem';
import { SpectrumAnalyzer } from './components/SpectrumAnalyzer';
import { Play, Pause, Square, Mic, Upload, Download, Sparkles, AlertCircle, Globe, Plus, Cpu, Ruler, ZoomIn, ZoomOut, MoveHorizontal, ChevronDown } from 'lucide-react';

const MAX_FILE_SIZE_BYTES = 200 * 1024 * 1024; // 200 MB
const MAX_AI_DURATION_SEC = 300; // 5 minutes

export default function App() {
const [lang, setLang] = useState<Language>('ja');
const t = translations[lang];
Expand Down Expand Up @@ -101,6 +104,12 @@ export default function App() {
const file = e.target.files?.[0];
if (!file) return;

if (file.size > MAX_FILE_SIZE_BYTES) {
setErrorMessage('File size exceeds 200MB limit. Please use a smaller file.');
e.target.value = '';
return;
}

setIsProcessing(true);
try {
initAudioContext();
Expand All @@ -116,7 +125,7 @@ export default function App() {
if (lowerName.endsWith('.wav')) {
detectedWavInfo = parseWavHeader(arrayBuffer);
} else if (lowerName.match(/\.(m4a|mp4|aac)$/)) {
try { detectedM4aInfo = parseM4aHeader(arrayBuffer); } catch (e) { console.warn(e); }
try { detectedM4aInfo = parseM4aHeader(arrayBuffer); } catch { /* ignore optional metadata parse errors */ }
m4aCodecInfo = detectM4aCodec(arrayBuffer);
}

Expand Down Expand Up @@ -170,7 +179,6 @@ export default function App() {
});

} catch (err) {
console.error(err);
setErrorMessage(t.decodeError);
} finally {
setIsProcessing(false);
Expand Down Expand Up @@ -306,7 +314,7 @@ export default function App() {

const handleStop = () => {
sourceNodesRef.current.forEach(node => {
try { node.stop(); } catch(e) {}
try { node.stop(); } catch { /* already stopped or not yet started */ }
});
sourceNodesRef.current = [];
if (animationFrameRef.current) cancelAnimationFrame(animationFrameRef.current);
Expand All @@ -333,6 +341,12 @@ export default function App() {

const handleAiAction = async (task: 'transcribe' | 'summarize') => {
if (!audioContextRef.current || tracks.length === 0) return;

if (editorState.duration > MAX_AI_DURATION_SEC) {
setErrorMessage('Audio too long for AI analysis. Maximum is 5 minutes (300s).');
return;
}

setIsProcessing(true);
setAiResult(null);

Expand All @@ -345,7 +359,6 @@ export default function App() {
summary: task === 'summarize' ? text : undefined
});
} catch (err) {
console.error(err);
setErrorMessage("AI Analysis Failed. Check API Key or Audio length.");
} finally {
setIsProcessing(false);
Expand Down Expand Up @@ -605,4 +618,4 @@ export default function App() {
</footer>
</div>
);
}
}
Loading