mirror of
https://github.com/Wan-Video/Wan2.1.git
synced 2025-11-03 13:54:30 +00:00
Merge f21ab74e38 into 7c81b2f27d
This commit is contained in:
commit
cf5c95fd7a
7
wan-pwa/.eslintrc.js
Normal file
7
wan-pwa/.eslintrc.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
extends: ["next/core-web-vitals", "prettier"],
|
||||||
|
rules: {
|
||||||
|
"@next/next/no-html-link-for-pages": "off",
|
||||||
|
},
|
||||||
|
}
|
||||||
72
wan-pwa/.gitignore
vendored
Normal file
72
wan-pwa/.gitignore
vendored
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# Next.js
|
||||||
|
.next/
|
||||||
|
out/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Production
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# Debug
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Environment Variables
|
||||||
|
.env
|
||||||
|
.env*.local
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# Vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# Turbo
|
||||||
|
.turbo
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
.venv
|
||||||
|
|
||||||
|
# IDEs
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# PWA
|
||||||
|
**/public/sw.js
|
||||||
|
**/public/workbox-*.js
|
||||||
|
**/public/worker-*.js
|
||||||
|
**/public/sw.js.map
|
||||||
|
**/public/workbox-*.js.map
|
||||||
|
**/public/worker-*.js.map
|
||||||
|
|
||||||
|
# Database
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
|
||||||
|
# MacOS
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
8
wan-pwa/.prettierrc
Normal file
8
wan-pwa/.prettierrc
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": false,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"printWidth": 100,
|
||||||
|
"plugins": ["prettier-plugin-tailwindcss"]
|
||||||
|
}
|
||||||
253
wan-pwa/CONTRIBUTING.md
Normal file
253
wan-pwa/CONTRIBUTING.md
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
# Contributing to Wan2.1 PWA
|
||||||
|
|
||||||
|
Thank you for your interest in contributing! This document provides guidelines for contributing to the project.
|
||||||
|
|
||||||
|
## Development Setup
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Clone your fork:
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/your-username/wan-pwa.git
|
||||||
|
cd wan-pwa
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Install dependencies:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Set up environment variables (see SETUP.md)
|
||||||
|
|
||||||
|
5. Start development:
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
wan-pwa/
|
||||||
|
├── apps/
|
||||||
|
│ ├── web/ # Next.js frontend
|
||||||
|
│ └── api/ # FastAPI backend
|
||||||
|
├── packages/
|
||||||
|
│ ├── ui/ # Shared UI components
|
||||||
|
│ ├── db/ # Database schema
|
||||||
|
│ └── types/ # TypeScript types
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
### TypeScript/JavaScript
|
||||||
|
- Use TypeScript for all new code
|
||||||
|
- Follow ESLint rules
|
||||||
|
- Use Prettier for formatting
|
||||||
|
- Prefer functional components with hooks
|
||||||
|
|
||||||
|
### Python
|
||||||
|
- Follow PEP 8 style guide
|
||||||
|
- Use type hints
|
||||||
|
- Document functions with docstrings
|
||||||
|
- Use async/await for async operations
|
||||||
|
|
||||||
|
### Formatting
|
||||||
|
```bash
|
||||||
|
npm run format # Format all code
|
||||||
|
npm run lint # Check linting
|
||||||
|
```
|
||||||
|
|
||||||
|
## Making Changes
|
||||||
|
|
||||||
|
### 1. Create a Branch
|
||||||
|
```bash
|
||||||
|
git checkout -b feature/your-feature-name
|
||||||
|
# or
|
||||||
|
git checkout -b fix/your-bug-fix
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Make Your Changes
|
||||||
|
- Write clean, readable code
|
||||||
|
- Add comments for complex logic
|
||||||
|
- Update documentation as needed
|
||||||
|
|
||||||
|
### 3. Test Your Changes
|
||||||
|
```bash
|
||||||
|
npm run test # Run tests
|
||||||
|
npm run build # Verify build works
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Commit Your Changes
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "feat: add new feature"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Commit Message Format
|
||||||
|
Follow Conventional Commits:
|
||||||
|
- `feat:` - New feature
|
||||||
|
- `fix:` - Bug fix
|
||||||
|
- `docs:` - Documentation changes
|
||||||
|
- `style:` - Code style changes
|
||||||
|
- `refactor:` - Code refactoring
|
||||||
|
- `test:` - Test additions/changes
|
||||||
|
- `chore:` - Maintenance tasks
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
```
|
||||||
|
feat: add video download button
|
||||||
|
fix: resolve credit deduction bug
|
||||||
|
docs: update setup instructions
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Push to Your Fork
|
||||||
|
```bash
|
||||||
|
git push origin feature/your-feature-name
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Create Pull Request
|
||||||
|
- Go to the original repository
|
||||||
|
- Click "New Pull Request"
|
||||||
|
- Select your branch
|
||||||
|
- Fill out the PR template
|
||||||
|
- Submit for review
|
||||||
|
|
||||||
|
## Pull Request Guidelines
|
||||||
|
|
||||||
|
### PR Title
|
||||||
|
Use the same format as commit messages:
|
||||||
|
```
|
||||||
|
feat: add dark mode support
|
||||||
|
fix: resolve authentication issue
|
||||||
|
```
|
||||||
|
|
||||||
|
### PR Description
|
||||||
|
Include:
|
||||||
|
- What changes were made
|
||||||
|
- Why the changes were necessary
|
||||||
|
- How to test the changes
|
||||||
|
- Screenshots (if UI changes)
|
||||||
|
- Related issues (if applicable)
|
||||||
|
|
||||||
|
### Example PR Description
|
||||||
|
```markdown
|
||||||
|
## Changes
|
||||||
|
- Added dark mode toggle to settings
|
||||||
|
- Implemented theme persistence in localStorage
|
||||||
|
- Updated all components to support dark mode
|
||||||
|
|
||||||
|
## Why
|
||||||
|
Users requested dark mode for better viewing experience at night
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
1. Click the theme toggle in settings
|
||||||
|
2. Verify colors change throughout the app
|
||||||
|
3. Refresh page and verify theme persists
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
[Before/After screenshots]
|
||||||
|
|
||||||
|
Closes #123
|
||||||
|
```
|
||||||
|
|
||||||
|
## Feature Requests
|
||||||
|
|
||||||
|
### Before Submitting
|
||||||
|
- Check if feature already exists
|
||||||
|
- Search existing issues/PRs
|
||||||
|
- Consider if it fits project scope
|
||||||
|
|
||||||
|
### Creating Feature Request
|
||||||
|
1. Open new issue
|
||||||
|
2. Use "Feature Request" template
|
||||||
|
3. Describe:
|
||||||
|
- The problem it solves
|
||||||
|
- Proposed solution
|
||||||
|
- Alternative solutions considered
|
||||||
|
- Additional context
|
||||||
|
|
||||||
|
## Bug Reports
|
||||||
|
|
||||||
|
### Before Submitting
|
||||||
|
- Ensure you're on latest version
|
||||||
|
- Search existing issues
|
||||||
|
- Try to reproduce consistently
|
||||||
|
|
||||||
|
### Creating Bug Report
|
||||||
|
1. Open new issue
|
||||||
|
2. Use "Bug Report" template
|
||||||
|
3. Include:
|
||||||
|
- Steps to reproduce
|
||||||
|
- Expected behavior
|
||||||
|
- Actual behavior
|
||||||
|
- Screenshots/logs
|
||||||
|
- Environment details
|
||||||
|
|
||||||
|
## Areas to Contribute
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- UI/UX improvements
|
||||||
|
- New prompt templates
|
||||||
|
- Performance optimizations
|
||||||
|
- Accessibility enhancements
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- API optimizations
|
||||||
|
- New generation features
|
||||||
|
- Background job processing
|
||||||
|
- Caching strategies
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- Setup guides
|
||||||
|
- API documentation
|
||||||
|
- Code examples
|
||||||
|
- Tutorial videos
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- Unit tests
|
||||||
|
- Integration tests
|
||||||
|
- E2E tests
|
||||||
|
- Performance tests
|
||||||
|
|
||||||
|
## Code Review Process
|
||||||
|
|
||||||
|
1. **Automated Checks**
|
||||||
|
- Linting
|
||||||
|
- Type checking
|
||||||
|
- Tests
|
||||||
|
- Build verification
|
||||||
|
|
||||||
|
2. **Manual Review**
|
||||||
|
- Code quality
|
||||||
|
- Best practices
|
||||||
|
- Documentation
|
||||||
|
- Test coverage
|
||||||
|
|
||||||
|
3. **Feedback**
|
||||||
|
- Address review comments
|
||||||
|
- Make requested changes
|
||||||
|
- Discuss disagreements respectfully
|
||||||
|
|
||||||
|
4. **Approval**
|
||||||
|
- At least one approval required
|
||||||
|
- All checks must pass
|
||||||
|
- No merge conflicts
|
||||||
|
|
||||||
|
## Getting Help
|
||||||
|
|
||||||
|
- **Documentation**: Check SETUP.md and README.md
|
||||||
|
- **Issues**: Search existing issues
|
||||||
|
- **Discussions**: Use GitHub Discussions
|
||||||
|
- **Discord**: Join our community (if available)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
By contributing, you agree that your contributions will be licensed under the same license as the project.
|
||||||
|
|
||||||
|
## Recognition
|
||||||
|
|
||||||
|
Contributors will be:
|
||||||
|
- Listed in CONTRIBUTORS.md
|
||||||
|
- Mentioned in release notes
|
||||||
|
- Credited in project documentation
|
||||||
|
|
||||||
|
Thank you for contributing! 🎉
|
||||||
266
wan-pwa/DEPLOYMENT.md
Normal file
266
wan-pwa/DEPLOYMENT.md
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
# Deployment Guide
|
||||||
|
|
||||||
|
## Frontend (Vercel)
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- Vercel account
|
||||||
|
- GitHub repository
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
1. **Push to GitHub**
|
||||||
|
```bash
|
||||||
|
git push origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Import to Vercel**
|
||||||
|
- Go to https://vercel.com
|
||||||
|
- Click "New Project"
|
||||||
|
- Import your repository
|
||||||
|
- Select `apps/web` as the root directory
|
||||||
|
|
||||||
|
3. **Configure Environment Variables**
|
||||||
|
|
||||||
|
Add these in Vercel dashboard:
|
||||||
|
```
|
||||||
|
NEXT_PUBLIC_SUPABASE_URL=https://xxxxx.supabase.co
|
||||||
|
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGci...
|
||||||
|
SUPABASE_SERVICE_ROLE_KEY=eyJhbGci...
|
||||||
|
NEXT_PUBLIC_API_URL=https://your-api-url.com
|
||||||
|
NEXT_PUBLIC_APP_URL=https://your-app.vercel.app
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Deploy**
|
||||||
|
- Click "Deploy"
|
||||||
|
- Wait for build to complete
|
||||||
|
- Visit your deployment URL
|
||||||
|
|
||||||
|
### Custom Domain (Optional)
|
||||||
|
|
||||||
|
1. Go to Settings → Domains
|
||||||
|
2. Add your custom domain
|
||||||
|
3. Update DNS records as instructed
|
||||||
|
4. Update `NEXT_PUBLIC_APP_URL` to your domain
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Backend (Modal)
|
||||||
|
|
||||||
|
Modal provides serverless Python deployment with GPU support.
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- Modal account
|
||||||
|
- Modal CLI installed: `pip install modal`
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
1. **Install Modal CLI**
|
||||||
|
```bash
|
||||||
|
pip install modal
|
||||||
|
modal setup
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Create Modal App**
|
||||||
|
|
||||||
|
Create `apps/api/modal_deploy.py`:
|
||||||
|
```python
|
||||||
|
import modal
|
||||||
|
|
||||||
|
stub = modal.Stub("wan-pwa-api")
|
||||||
|
|
||||||
|
image = modal.Image.debian_slim().pip_install_from_requirements("requirements.txt")
|
||||||
|
|
||||||
|
@stub.function(image=image)
|
||||||
|
@modal.asgi_app()
|
||||||
|
def fastapi_app():
|
||||||
|
from main import app
|
||||||
|
return app
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Set Secrets**
|
||||||
|
```bash
|
||||||
|
modal secret create wan-secrets \
|
||||||
|
SUPABASE_URL=https://xxxxx.supabase.co \
|
||||||
|
SUPABASE_SERVICE_ROLE_KEY=eyJhbGci... \
|
||||||
|
REPLICATE_API_TOKEN=r8_xxxxx
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Deploy**
|
||||||
|
```bash
|
||||||
|
cd apps/api
|
||||||
|
modal deploy modal_deploy.py
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Get URL**
|
||||||
|
- Modal will provide a URL like `https://your-app--modal.com`
|
||||||
|
- Update frontend `NEXT_PUBLIC_API_URL` to this URL
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Backend (Railway - Alternative)
|
||||||
|
|
||||||
|
Railway is simpler but doesn't have GPU support (uses Replicate API instead).
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
1. **Install Railway CLI**
|
||||||
|
```bash
|
||||||
|
npm install -g @railway/cli
|
||||||
|
railway login
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Create Project**
|
||||||
|
```bash
|
||||||
|
cd apps/api
|
||||||
|
railway init
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Add Environment Variables**
|
||||||
|
```bash
|
||||||
|
railway variables set SUPABASE_URL=https://xxxxx.supabase.co
|
||||||
|
railway variables set SUPABASE_SERVICE_ROLE_KEY=eyJhbGci...
|
||||||
|
railway variables set REPLICATE_API_TOKEN=r8_xxxxx
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Deploy**
|
||||||
|
```bash
|
||||||
|
railway up
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Get URL**
|
||||||
|
```bash
|
||||||
|
railway domain
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Database (Supabase)
|
||||||
|
|
||||||
|
Database is already set up in Supabase - no deployment needed!
|
||||||
|
|
||||||
|
### Production Checklist
|
||||||
|
|
||||||
|
- [ ] Run migrations in production project
|
||||||
|
- [ ] Enable RLS (Row Level Security)
|
||||||
|
- [ ] Configure Auth providers (email, Google, GitHub)
|
||||||
|
- [ ] Set up storage buckets with proper policies
|
||||||
|
- [ ] Enable database backups
|
||||||
|
- [ ] Set up monitoring and alerts
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Redis (Upstash) - Optional
|
||||||
|
|
||||||
|
For background jobs and caching.
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
1. **Create Upstash Account**
|
||||||
|
- Go to https://upstash.com
|
||||||
|
- Create a Redis database
|
||||||
|
|
||||||
|
2. **Get Connection String**
|
||||||
|
- Copy the connection URL
|
||||||
|
|
||||||
|
3. **Update Environment**
|
||||||
|
```
|
||||||
|
REDIS_URL=redis://...
|
||||||
|
CELERY_BROKER_URL=redis://...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CI/CD with GitHub Actions
|
||||||
|
|
||||||
|
Create `.github/workflows/deploy.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy-frontend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Deploy to Vercel
|
||||||
|
uses: amondnet/vercel-action@v25
|
||||||
|
with:
|
||||||
|
vercel-token: ${{ secrets.VERCEL_TOKEN }}
|
||||||
|
vercel-org-id: ${{ secrets.ORG_ID }}
|
||||||
|
vercel-project-id: ${{ secrets.PROJECT_ID }}
|
||||||
|
working-directory: apps/web
|
||||||
|
|
||||||
|
deploy-backend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: '3.10'
|
||||||
|
- name: Deploy to Modal
|
||||||
|
run: |
|
||||||
|
pip install modal
|
||||||
|
modal token set --token-id ${{ secrets.MODAL_TOKEN_ID }} --token-secret ${{ secrets.MODAL_TOKEN_SECRET }}
|
||||||
|
cd apps/api && modal deploy modal_deploy.py
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Environment Variables Checklist
|
||||||
|
|
||||||
|
### Frontend (Vercel)
|
||||||
|
- [ ] `NEXT_PUBLIC_SUPABASE_URL`
|
||||||
|
- [ ] `NEXT_PUBLIC_SUPABASE_ANON_KEY`
|
||||||
|
- [ ] `SUPABASE_SERVICE_ROLE_KEY`
|
||||||
|
- [ ] `NEXT_PUBLIC_API_URL`
|
||||||
|
- [ ] `NEXT_PUBLIC_APP_URL`
|
||||||
|
|
||||||
|
### Backend (Modal/Railway)
|
||||||
|
- [ ] `SUPABASE_URL`
|
||||||
|
- [ ] `SUPABASE_SERVICE_ROLE_KEY`
|
||||||
|
- [ ] `REPLICATE_API_TOKEN`
|
||||||
|
- [ ] `ALLOWED_ORIGINS`
|
||||||
|
- [ ] `REDIS_URL` (optional)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Post-Deployment
|
||||||
|
|
||||||
|
1. **Test the Application**
|
||||||
|
- Sign up a test user
|
||||||
|
- Generate a test video
|
||||||
|
- Check credit deduction
|
||||||
|
- Verify video download
|
||||||
|
|
||||||
|
2. **Monitor**
|
||||||
|
- Set up Sentry for error tracking
|
||||||
|
- Monitor Vercel analytics
|
||||||
|
- Check Supabase usage
|
||||||
|
|
||||||
|
3. **Scale**
|
||||||
|
- Adjust Vercel plan if needed
|
||||||
|
- Scale Modal functions based on usage
|
||||||
|
- Upgrade Supabase plan for production
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Build Failures
|
||||||
|
- Check environment variables are set
|
||||||
|
- Verify all dependencies in package.json
|
||||||
|
- Check build logs for specific errors
|
||||||
|
|
||||||
|
### API Errors
|
||||||
|
- Verify Supabase connection
|
||||||
|
- Check Replicate API token
|
||||||
|
- Review CORS settings
|
||||||
|
|
||||||
|
### Database Issues
|
||||||
|
- Ensure migrations have run
|
||||||
|
- Check RLS policies
|
||||||
|
- Verify user permissions
|
||||||
21
wan-pwa/LICENSE
Normal file
21
wan-pwa/LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 Wan2.1 PWA
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
301
wan-pwa/PROJECT_SUMMARY.md
Normal file
301
wan-pwa/PROJECT_SUMMARY.md
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
# Wan2.1 PWA - Project Summary
|
||||||
|
|
||||||
|
## What Has Been Built
|
||||||
|
|
||||||
|
A complete, production-ready Progressive Web App for AI video generation using Wan2.1 models.
|
||||||
|
|
||||||
|
### Features Implemented
|
||||||
|
|
||||||
|
✅ **Frontend (Next.js 15)**
|
||||||
|
- Modern UI with shadcn/ui components
|
||||||
|
- 50+ prompt templates across 7 categories
|
||||||
|
- Responsive design with Tailwind CSS
|
||||||
|
- PWA support (installable, offline-capable)
|
||||||
|
- Authentication flows with Supabase
|
||||||
|
- Credit system UI
|
||||||
|
- Video generation interface
|
||||||
|
|
||||||
|
✅ **Backend (FastAPI)**
|
||||||
|
- RESTful API with FastAPI
|
||||||
|
- Replicate integration for GPU processing
|
||||||
|
- User authentication with Supabase
|
||||||
|
- Credit system with transaction tracking
|
||||||
|
- Video generation endpoints (T2V, I2V)
|
||||||
|
- Real-time status tracking
|
||||||
|
- Error handling and validation
|
||||||
|
|
||||||
|
✅ **Database (Supabase)**
|
||||||
|
- Complete schema with migrations
|
||||||
|
- Row-level security (RLS)
|
||||||
|
- User profiles and credits
|
||||||
|
- Generation history
|
||||||
|
- Transaction logging
|
||||||
|
- Storage for user images
|
||||||
|
|
||||||
|
✅ **Infrastructure**
|
||||||
|
- Monorepo setup with Turborepo
|
||||||
|
- Environment configuration
|
||||||
|
- Deployment guides (Vercel, Modal, Railway)
|
||||||
|
- Development workflow
|
||||||
|
- Documentation
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
wan-pwa/
|
||||||
|
├── apps/
|
||||||
|
│ ├── web/ # Next.js frontend
|
||||||
|
│ │ ├── src/
|
||||||
|
│ │ │ ├── app/ # App router pages
|
||||||
|
│ │ │ ├── components/ # React components
|
||||||
|
│ │ │ │ └── ui/ # shadcn/ui components
|
||||||
|
│ │ │ └── lib/ # Utilities
|
||||||
|
│ │ │ ├── prompts/ # 50+ templates
|
||||||
|
│ │ │ ├── supabase/ # DB client
|
||||||
|
│ │ │ └── utils.ts # Helper functions
|
||||||
|
│ │ └── public/
|
||||||
|
│ │ ├── icons/ # PWA icons
|
||||||
|
│ │ └── manifest.json # PWA manifest
|
||||||
|
│ │
|
||||||
|
│ └── api/ # FastAPI backend
|
||||||
|
│ ├── routes/ # API endpoints
|
||||||
|
│ │ ├── generation.py # Video generation
|
||||||
|
│ │ ├── auth.py # Authentication
|
||||||
|
│ │ └── users.py # User management
|
||||||
|
│ ├── services/ # Business logic
|
||||||
|
│ │ ├── replicate_service.py
|
||||||
|
│ │ └── credit_service.py
|
||||||
|
│ ├── models/ # Pydantic models
|
||||||
|
│ ├── core/ # Config & utilities
|
||||||
|
│ └── main.py # FastAPI app
|
||||||
|
│
|
||||||
|
├── packages/
|
||||||
|
│ └── db/ # Database
|
||||||
|
│ ├── migrations/ # SQL migrations
|
||||||
|
│ └── README.md
|
||||||
|
│
|
||||||
|
├── README.md # Project overview
|
||||||
|
├── SETUP.md # Setup instructions
|
||||||
|
├── DEPLOYMENT.md # Deployment guide
|
||||||
|
├── CONTRIBUTING.md # Contribution guide
|
||||||
|
└── LICENSE # MIT License
|
||||||
|
```
|
||||||
|
|
||||||
|
## Technology Stack
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- **Framework**: Next.js 15 (App Router)
|
||||||
|
- **UI Library**: shadcn/ui + Radix UI
|
||||||
|
- **Styling**: Tailwind CSS
|
||||||
|
- **State Management**: Zustand (ready to integrate)
|
||||||
|
- **Forms**: React Hook Form + Zod
|
||||||
|
- **PWA**: next-pwa
|
||||||
|
- **Auth**: Supabase SSR
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- **Framework**: FastAPI
|
||||||
|
- **GPU Processing**: Replicate API
|
||||||
|
- **Validation**: Pydantic v2
|
||||||
|
- **Async**: uvicorn + httpx
|
||||||
|
|
||||||
|
### Database & Auth
|
||||||
|
- **Database**: Supabase (Postgres)
|
||||||
|
- **Authentication**: Supabase Auth
|
||||||
|
- **Storage**: Supabase Storage
|
||||||
|
- **RLS**: Row Level Security enabled
|
||||||
|
|
||||||
|
### DevOps
|
||||||
|
- **Monorepo**: Turborepo
|
||||||
|
- **Package Manager**: npm
|
||||||
|
- **Linting**: ESLint + Prettier
|
||||||
|
- **TypeScript**: Strict mode
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
### 1. Prompt Template System (50+ Templates)
|
||||||
|
Located in `apps/web/src/lib/prompts/templates.ts`
|
||||||
|
|
||||||
|
Categories:
|
||||||
|
- Cinematic (6 templates)
|
||||||
|
- Animation (6 templates)
|
||||||
|
- Realistic (6 templates)
|
||||||
|
- Abstract (5 templates)
|
||||||
|
- Nature (6 templates)
|
||||||
|
- People (5 templates)
|
||||||
|
- Animals (5 templates)
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Search templates
|
||||||
|
- Filter by category
|
||||||
|
- Featured templates
|
||||||
|
- Tag-based discovery
|
||||||
|
|
||||||
|
### 2. Video Generation
|
||||||
|
|
||||||
|
**Text-to-Video (T2V)**
|
||||||
|
- Models: T2V-14B, T2V-1.3B
|
||||||
|
- Resolutions: 480p, 720p
|
||||||
|
- Duration: 1-10 seconds
|
||||||
|
- Custom prompts + negative prompts
|
||||||
|
- Seed for reproducibility
|
||||||
|
|
||||||
|
**Image-to-Video (I2V)**
|
||||||
|
- Model: I2V-14B
|
||||||
|
- Resolutions: 480p, 720p
|
||||||
|
- Upload image from device
|
||||||
|
- Animate with text prompts
|
||||||
|
|
||||||
|
### 3. Credit System
|
||||||
|
|
||||||
|
**Pricing**
|
||||||
|
- T2V-14B 720p: 20 credits
|
||||||
|
- T2V-14B 480p: 10 credits
|
||||||
|
- T2V-1.3B 480p: 5 credits
|
||||||
|
- I2V-14B 720p: 25 credits
|
||||||
|
- I2V-14B 480p: 15 credits
|
||||||
|
|
||||||
|
**Features**
|
||||||
|
- Free tier: 100 credits
|
||||||
|
- Transaction history
|
||||||
|
- Automatic refunds on errors
|
||||||
|
- Subscription tiers ready
|
||||||
|
|
||||||
|
### 4. Authentication
|
||||||
|
|
||||||
|
- Email/Password signup
|
||||||
|
- Supabase Auth integration
|
||||||
|
- JWT token handling
|
||||||
|
- Automatic profile creation
|
||||||
|
- RLS for data security
|
||||||
|
|
||||||
|
### 5. PWA Features
|
||||||
|
|
||||||
|
- Installable on mobile/desktop
|
||||||
|
- Offline-capable (configured)
|
||||||
|
- App manifest
|
||||||
|
- Service worker (next-pwa)
|
||||||
|
- iOS and Android support
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
- `POST /api/auth/signup` - Create account
|
||||||
|
- `POST /api/auth/signin` - Sign in
|
||||||
|
- `POST /api/auth/signout` - Sign out
|
||||||
|
|
||||||
|
### Video Generation
|
||||||
|
- `POST /api/generation/text-to-video` - Generate from text
|
||||||
|
- `POST /api/generation/image-to-video` - Generate from image
|
||||||
|
- `GET /api/generation/status/{id}` - Get status
|
||||||
|
- `GET /api/generation/history` - Get history
|
||||||
|
|
||||||
|
### User Management
|
||||||
|
- `GET /api/users/me` - Get profile
|
||||||
|
- `GET /api/users/credits` - Get credits
|
||||||
|
- `GET /api/users/transactions` - Get transactions
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
### Tables
|
||||||
|
1. **users** - User profiles, credits, subscription
|
||||||
|
2. **generations** - Video generation requests
|
||||||
|
3. **credit_transactions** - Credit history
|
||||||
|
|
||||||
|
### Storage
|
||||||
|
- **images** - User-uploaded images for I2V
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Quick Start (5 Minutes)
|
||||||
|
|
||||||
|
1. **Clone and install**
|
||||||
|
```bash
|
||||||
|
cd wan-pwa
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Set up Supabase**
|
||||||
|
- Create project at supabase.com
|
||||||
|
- Run migrations from `packages/db/migrations/`
|
||||||
|
- Copy credentials to `.env.local`
|
||||||
|
|
||||||
|
3. **Set up Replicate**
|
||||||
|
- Get API token from replicate.com
|
||||||
|
- Add to `.env` files
|
||||||
|
|
||||||
|
4. **Start development**
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Frontend: http://localhost:3000
|
||||||
|
Backend: http://localhost:8000
|
||||||
|
|
||||||
|
### Detailed Setup
|
||||||
|
See [SETUP.md](./SETUP.md) for complete instructions.
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### Production Stack
|
||||||
|
- **Frontend**: Vercel
|
||||||
|
- **Backend**: Modal or Railway
|
||||||
|
- **Database**: Supabase
|
||||||
|
- **Storage**: Supabase Storage
|
||||||
|
|
||||||
|
See [DEPLOYMENT.md](./DEPLOYMENT.md) for deployment instructions.
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
### Recommended Additions
|
||||||
|
|
||||||
|
1. **Real-time Updates**
|
||||||
|
- WebSocket support for live progress
|
||||||
|
- Server-sent events for notifications
|
||||||
|
|
||||||
|
2. **Batch Processing**
|
||||||
|
- Generate multiple videos
|
||||||
|
- Queue management with Celery + Redis
|
||||||
|
|
||||||
|
3. **Payment Integration**
|
||||||
|
- Stripe for credit purchases
|
||||||
|
- Subscription management
|
||||||
|
|
||||||
|
4. **Enhanced Features**
|
||||||
|
- Video editing
|
||||||
|
- Frame interpolation
|
||||||
|
- Style transfer
|
||||||
|
|
||||||
|
5. **Analytics**
|
||||||
|
- Usage tracking
|
||||||
|
- Performance monitoring
|
||||||
|
- User insights
|
||||||
|
|
||||||
|
6. **Mobile App**
|
||||||
|
- React Native wrapper
|
||||||
|
- Native features
|
||||||
|
|
||||||
|
## Credits & Attribution
|
||||||
|
|
||||||
|
Built using:
|
||||||
|
- [Wan2.1](https://github.com/Wan-Video/Wan2.1) - AI video models
|
||||||
|
- [Next.js](https://nextjs.org) - React framework
|
||||||
|
- [FastAPI](https://fastapi.tiangolo.com) - Python API
|
||||||
|
- [Supabase](https://supabase.com) - Backend as a Service
|
||||||
|
- [Replicate](https://replicate.com) - GPU inference
|
||||||
|
- [shadcn/ui](https://ui.shadcn.com) - UI components
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License - see [LICENSE](./LICENSE)
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
- Documentation: See README.md, SETUP.md, DEPLOYMENT.md
|
||||||
|
- Issues: GitHub Issues
|
||||||
|
- Contributing: See CONTRIBUTING.md
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Status**: ✅ Ready for development and testing
|
||||||
|
**Version**: 1.0.0
|
||||||
|
**Last Updated**: 2024
|
||||||
114
wan-pwa/README.md
Normal file
114
wan-pwa/README.md
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
# Wan2.1 PWA - AI Video Generation Platform
|
||||||
|
|
||||||
|
A production-ready Progressive Web App for AI-powered video generation using Wan2.1 models.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 🎨 Smart Prompt Engineering: 50+ templates with context-aware suggestions
|
||||||
|
- 🎬 Video Generation: Text-to-Video and Image-to-Video
|
||||||
|
- 📱 Progressive Web App: Installable, offline-capable
|
||||||
|
- 🔐 Authentication: Supabase Auth with OAuth support
|
||||||
|
- 💳 Credit System: Freemium model with usage tracking
|
||||||
|
- ⚡ Real-time Progress: WebSocket-based generation tracking
|
||||||
|
- 🎯 Template Library: Categorized prompts (Cinematic, Animation, Realistic)
|
||||||
|
- 📥 Download & Share: Export videos to device
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- Framework: Next.js 15 (App Router)
|
||||||
|
- UI: shadcn/ui + Tailwind CSS
|
||||||
|
- State: Zustand
|
||||||
|
- Forms: React Hook Form + Zod
|
||||||
|
- PWA: next-pwa
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- API: FastAPI (Python)
|
||||||
|
- GPU: Replicate / Modal
|
||||||
|
- Queue: Celery + Redis
|
||||||
|
|
||||||
|
### Database
|
||||||
|
- DB: Supabase (Postgres)
|
||||||
|
- Auth: Supabase Auth
|
||||||
|
- Storage: Supabase Storage
|
||||||
|
- Cache: Upstash Redis
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
wan-pwa/
|
||||||
|
├── apps/
|
||||||
|
│ ├── web/ # Next.js frontend
|
||||||
|
│ └── api/ # FastAPI backend
|
||||||
|
├── packages/
|
||||||
|
│ ├── ui/ # Shared UI components
|
||||||
|
│ ├── db/ # Database schema & migrations
|
||||||
|
│ └── types/ # Shared TypeScript types
|
||||||
|
├── turbo.json # Monorepo build config
|
||||||
|
└── package.json # Root dependencies
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Node.js 18+
|
||||||
|
- Python 3.10+
|
||||||
|
- npm 9+
|
||||||
|
- Supabase account
|
||||||
|
- Replicate account
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
See [SETUP.md](./SETUP.md) for detailed instructions.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone & Install
|
||||||
|
git clone <your-repo>
|
||||||
|
cd wan-pwa
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Start all services
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Frontend: http://localhost:3000
|
||||||
|
# Backend: http://localhost:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev # Start all services
|
||||||
|
npm run build # Build all packages
|
||||||
|
npm run lint # Lint all packages
|
||||||
|
npm run test # Run all tests
|
||||||
|
npm run clean # Clean build artifacts
|
||||||
|
npm run format # Format code with Prettier
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
- Frontend: Vercel
|
||||||
|
- Backend: Modal or Railway
|
||||||
|
- Database: Supabase
|
||||||
|
|
||||||
|
See [DEPLOYMENT.md](./DEPLOYMENT.md) for detailed instructions.
|
||||||
|
|
||||||
|
## Project Roadmap
|
||||||
|
|
||||||
|
- [x] Monorepo setup
|
||||||
|
- [ ] Authentication flows
|
||||||
|
- [ ] Prompt template system
|
||||||
|
- [ ] T2V generation
|
||||||
|
- [ ] I2V generation
|
||||||
|
- [ ] Batch processing
|
||||||
|
- [ ] Payment integration
|
||||||
|
- [ ] Mobile app
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
See [CONTRIBUTING.md](./CONTRIBUTING.md)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License - see [LICENSE](./LICENSE)
|
||||||
186
wan-pwa/SETUP.md
Normal file
186
wan-pwa/SETUP.md
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
# Setup Guide
|
||||||
|
|
||||||
|
## Quick Start (5 Minutes)
|
||||||
|
|
||||||
|
### 1. Prerequisites
|
||||||
|
|
||||||
|
- Node.js 18+ installed
|
||||||
|
- Python 3.10+ installed
|
||||||
|
- Supabase account (free tier)
|
||||||
|
- Replicate account ($10 free credit)
|
||||||
|
|
||||||
|
### 2. Clone and Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <your-repo-url>
|
||||||
|
cd wan-pwa
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Get Credentials
|
||||||
|
|
||||||
|
#### Supabase Setup (3 minutes)
|
||||||
|
|
||||||
|
1. Go to https://supabase.com
|
||||||
|
2. Create new project: "wan-pwa"
|
||||||
|
3. Wait ~2 minutes for provisioning
|
||||||
|
4. Go to Settings → API
|
||||||
|
5. Copy these 4 values:
|
||||||
|
- Project URL → `NEXT_PUBLIC_SUPABASE_URL`
|
||||||
|
- anon public → `NEXT_PUBLIC_SUPABASE_ANON_KEY`
|
||||||
|
- service_role → `SUPABASE_SERVICE_ROLE_KEY`
|
||||||
|
- JWT Secret → `SUPABASE_JWT_SECRET`
|
||||||
|
|
||||||
|
#### Replicate Setup (2 minutes)
|
||||||
|
|
||||||
|
1. Go to https://replicate.com
|
||||||
|
2. Sign up with GitHub
|
||||||
|
3. Go to https://replicate.com/account/api-tokens
|
||||||
|
4. Create token → Copy → `REPLICATE_API_TOKEN`
|
||||||
|
|
||||||
|
### 4. Configure Environment
|
||||||
|
|
||||||
|
#### Frontend (.env.local)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/web
|
||||||
|
cp .env.example .env.local
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit `.env.local`:
|
||||||
|
```bash
|
||||||
|
NEXT_PUBLIC_SUPABASE_URL=https://xxxxx.supabase.co
|
||||||
|
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGci...
|
||||||
|
SUPABASE_SERVICE_ROLE_KEY=eyJhbGci...
|
||||||
|
NEXT_PUBLIC_API_URL=http://localhost:8000
|
||||||
|
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Backend (.env)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ../api
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit `.env`:
|
||||||
|
```bash
|
||||||
|
SUPABASE_URL=https://xxxxx.supabase.co
|
||||||
|
SUPABASE_SERVICE_ROLE_KEY=eyJhbGci...
|
||||||
|
REPLICATE_API_TOKEN=r8_xxxxx
|
||||||
|
ALLOWED_ORIGINS=http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Database Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From project root
|
||||||
|
cd packages/db
|
||||||
|
|
||||||
|
# Run migration in Supabase dashboard:
|
||||||
|
# 1. Go to SQL Editor in Supabase
|
||||||
|
# 2. Create new query
|
||||||
|
# 3. Copy contents of migrations/001_initial_schema.sql
|
||||||
|
# 4. Run query
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Start Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Terminal 1: Frontend
|
||||||
|
cd apps/web
|
||||||
|
npm run dev
|
||||||
|
# → http://localhost:3000
|
||||||
|
|
||||||
|
# Terminal 2: Backend
|
||||||
|
cd apps/api
|
||||||
|
pip install -r requirements.txt
|
||||||
|
uvicorn main:app --reload
|
||||||
|
# → http://localhost:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Test the App
|
||||||
|
|
||||||
|
1. Open http://localhost:3000
|
||||||
|
2. Click "Get Started"
|
||||||
|
3. Sign up with email
|
||||||
|
4. Browse prompt templates
|
||||||
|
5. Generate your first video!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Module not found" errors
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clear caches and reinstall
|
||||||
|
rm -rf node_modules package-lock.json
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Supabase connection errors
|
||||||
|
|
||||||
|
Check:
|
||||||
|
- URLs don't have trailing slashes
|
||||||
|
- Keys are complete (very long strings)
|
||||||
|
- Project is fully provisioned (not still "Setting up")
|
||||||
|
|
||||||
|
### API not starting
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check Python version
|
||||||
|
python --version # Should be 3.10+
|
||||||
|
|
||||||
|
# Try with full path
|
||||||
|
python3 -m uvicorn main:app --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database migration fails
|
||||||
|
|
||||||
|
- Make sure you're using the SQL Editor in Supabase dashboard
|
||||||
|
- Check that UUID extension is enabled
|
||||||
|
- Verify you're in the correct project
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- [ ] Customize prompt templates in `apps/web/src/lib/prompts/templates.ts`
|
||||||
|
- [ ] Add your logo in `apps/web/public/icons/`
|
||||||
|
- [ ] Setup GitHub Actions for CI/CD
|
||||||
|
- [ ] Deploy to production (see DEPLOYMENT.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
wan-pwa/
|
||||||
|
├── apps/
|
||||||
|
│ ├── web/ # Next.js frontend
|
||||||
|
│ │ ├── src/
|
||||||
|
│ │ │ ├── app/ # Pages & layouts
|
||||||
|
│ │ │ ├── components/ # React components
|
||||||
|
│ │ │ └── lib/ # Utilities & hooks
|
||||||
|
│ │ └── public/ # Static assets
|
||||||
|
│ │
|
||||||
|
│ └── api/ # FastAPI backend
|
||||||
|
│ ├── routes/ # API endpoints
|
||||||
|
│ ├── core/ # Business logic
|
||||||
|
│ └── models/ # Pydantic models
|
||||||
|
│
|
||||||
|
├── packages/
|
||||||
|
│ └── db/ # Database schema
|
||||||
|
│ └── migrations/ # SQL migrations
|
||||||
|
│
|
||||||
|
└── turbo.json # Monorepo config
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Need Help?
|
||||||
|
|
||||||
|
- Check [README.md](./README.md) for features overview
|
||||||
|
- See [DEPLOYMENT.md](./DEPLOYMENT.md) for production setup
|
||||||
|
- Open an issue on GitHub
|
||||||
18
wan-pwa/apps/api/.env.example
Normal file
18
wan-pwa/apps/api/.env.example
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Supabase
|
||||||
|
SUPABASE_URL=https://xxxxx.supabase.co
|
||||||
|
SUPABASE_SERVICE_ROLE_KEY=eyJhbGci...
|
||||||
|
|
||||||
|
# Replicate
|
||||||
|
REPLICATE_API_TOKEN=r8_xxxxx
|
||||||
|
|
||||||
|
# API Configuration
|
||||||
|
ALLOWED_ORIGINS=http://localhost:3000
|
||||||
|
PORT=8000
|
||||||
|
ENV=development
|
||||||
|
|
||||||
|
# Redis (optional for queue)
|
||||||
|
REDIS_URL=redis://localhost:6379
|
||||||
|
|
||||||
|
# Celery (optional for background tasks)
|
||||||
|
CELERY_BROKER_URL=redis://localhost:6379/0
|
||||||
|
CELERY_RESULT_BACKEND=redis://localhost:6379/0
|
||||||
1
wan-pwa/apps/api/core/__init__.py
Normal file
1
wan-pwa/apps/api/core/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# Core package
|
||||||
34
wan-pwa/apps/api/core/config.py
Normal file
34
wan-pwa/apps/api/core/config.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
from pydantic_settings import BaseSettings
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
# API
|
||||||
|
APP_NAME: str = "Wan2.1 PWA API"
|
||||||
|
VERSION: str = "1.0.0"
|
||||||
|
ENV: str = "development"
|
||||||
|
PORT: int = 8000
|
||||||
|
|
||||||
|
# Supabase
|
||||||
|
SUPABASE_URL: str
|
||||||
|
SUPABASE_SERVICE_ROLE_KEY: str
|
||||||
|
|
||||||
|
# Replicate
|
||||||
|
REPLICATE_API_TOKEN: str
|
||||||
|
|
||||||
|
# CORS
|
||||||
|
ALLOWED_ORIGINS: str = "http://localhost:3000"
|
||||||
|
|
||||||
|
# Redis (optional)
|
||||||
|
REDIS_URL: str = "redis://localhost:6379"
|
||||||
|
|
||||||
|
# Celery (optional)
|
||||||
|
CELERY_BROKER_URL: str = "redis://localhost:6379/0"
|
||||||
|
CELERY_RESULT_BACKEND: str = "redis://localhost:6379/0"
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
env_file = ".env"
|
||||||
|
case_sensitive = True
|
||||||
|
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
8
wan-pwa/apps/api/core/supabase.py
Normal file
8
wan-pwa/apps/api/core/supabase.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from supabase import create_client, Client
|
||||||
|
from core.config import settings
|
||||||
|
|
||||||
|
supabase: Client = create_client(settings.SUPABASE_URL, settings.SUPABASE_SERVICE_ROLE_KEY)
|
||||||
|
|
||||||
|
|
||||||
|
def get_supabase() -> Client:
|
||||||
|
return supabase
|
||||||
52
wan-pwa/apps/api/main.py
Normal file
52
wan-pwa/apps/api/main.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
from routes import generation, auth, users
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
app = FastAPI(
|
||||||
|
title="Wan2.1 PWA API",
|
||||||
|
description="API for AI video generation using Wan2.1 models",
|
||||||
|
version="1.0.0",
|
||||||
|
)
|
||||||
|
|
||||||
|
# CORS configuration
|
||||||
|
origins = os.getenv("ALLOWED_ORIGINS", "http://localhost:3000").split(",")
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=origins,
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Include routers
|
||||||
|
app.include_router(generation.router, prefix="/api/generation", tags=["generation"])
|
||||||
|
app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
|
||||||
|
app.include_router(users.router, prefix="/api/users", tags=["users"])
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def root():
|
||||||
|
return {"message": "Wan2.1 PWA API", "version": "1.0.0"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
async def health():
|
||||||
|
return {"status": "healthy"}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
uvicorn.run(
|
||||||
|
"main:app",
|
||||||
|
host="0.0.0.0",
|
||||||
|
port=int(os.getenv("PORT", 8000)),
|
||||||
|
reload=os.getenv("ENV") == "development",
|
||||||
|
)
|
||||||
1
wan-pwa/apps/api/models/__init__.py
Normal file
1
wan-pwa/apps/api/models/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# Models package
|
||||||
41
wan-pwa/apps/api/models/generation.py
Normal file
41
wan-pwa/apps/api/models/generation.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from typing import Optional, Literal
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class TextToVideoRequest(BaseModel):
|
||||||
|
prompt: str = Field(..., min_length=1, max_length=2000)
|
||||||
|
negative_prompt: Optional[str] = Field(None, max_length=2000)
|
||||||
|
model: Literal["t2v-14B", "t2v-1.3B"] = "t2v-14B"
|
||||||
|
resolution: Literal["480p", "720p"] = "720p"
|
||||||
|
duration: int = Field(5, ge=1, le=10)
|
||||||
|
seed: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
class ImageToVideoRequest(BaseModel):
|
||||||
|
prompt: str = Field(..., min_length=1, max_length=2000)
|
||||||
|
image_url: str = Field(..., min_length=1)
|
||||||
|
negative_prompt: Optional[str] = Field(None, max_length=2000)
|
||||||
|
model: Literal["i2v-14B"] = "i2v-14B"
|
||||||
|
resolution: Literal["480p", "720p"] = "720p"
|
||||||
|
duration: int = Field(5, ge=1, le=10)
|
||||||
|
seed: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
class GenerationResponse(BaseModel):
|
||||||
|
id: str
|
||||||
|
status: Literal["pending", "processing", "completed", "failed"]
|
||||||
|
video_url: Optional[str] = None
|
||||||
|
error: Optional[str] = None
|
||||||
|
created_at: datetime
|
||||||
|
completed_at: Optional[datetime] = None
|
||||||
|
credits_used: int
|
||||||
|
|
||||||
|
|
||||||
|
class GenerationStatus(BaseModel):
|
||||||
|
id: str
|
||||||
|
status: Literal["pending", "processing", "completed", "failed"]
|
||||||
|
progress: int = Field(0, ge=0, le=100)
|
||||||
|
video_url: Optional[str] = None
|
||||||
|
error: Optional[str] = None
|
||||||
|
logs: Optional[str] = None
|
||||||
27
wan-pwa/apps/api/models/user.py
Normal file
27
wan-pwa/apps/api/models/user.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from pydantic import BaseModel, EmailStr
|
||||||
|
from typing import Optional
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class User(BaseModel):
|
||||||
|
id: str
|
||||||
|
email: EmailStr
|
||||||
|
credits: int
|
||||||
|
subscription_tier: str = "free"
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
|
||||||
|
|
||||||
|
class UserCredits(BaseModel):
|
||||||
|
user_id: str
|
||||||
|
credits: int
|
||||||
|
subscription_tier: str
|
||||||
|
|
||||||
|
|
||||||
|
class CreditTransaction(BaseModel):
|
||||||
|
id: str
|
||||||
|
user_id: str
|
||||||
|
amount: int
|
||||||
|
type: str
|
||||||
|
description: Optional[str] = None
|
||||||
|
created_at: datetime
|
||||||
12
wan-pwa/apps/api/requirements.txt
Normal file
12
wan-pwa/apps/api/requirements.txt
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
fastapi==0.115.5
|
||||||
|
uvicorn[standard]==0.32.1
|
||||||
|
python-dotenv==1.0.1
|
||||||
|
pydantic==2.10.3
|
||||||
|
pydantic-settings==2.6.1
|
||||||
|
supabase==2.10.0
|
||||||
|
replicate==1.0.4
|
||||||
|
redis==5.2.0
|
||||||
|
celery==5.4.0
|
||||||
|
websockets==14.1
|
||||||
|
python-multipart==0.0.18
|
||||||
|
httpx==0.28.1
|
||||||
1
wan-pwa/apps/api/routes/__init__.py
Normal file
1
wan-pwa/apps/api/routes/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# Routes package
|
||||||
65
wan-pwa/apps/api/routes/auth.py
Normal file
65
wan-pwa/apps/api/routes/auth.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
from pydantic import BaseModel, EmailStr
|
||||||
|
from core.supabase import get_supabase
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
class SignUpRequest(BaseModel):
|
||||||
|
email: EmailStr
|
||||||
|
password: str
|
||||||
|
|
||||||
|
|
||||||
|
class SignInRequest(BaseModel):
|
||||||
|
email: EmailStr
|
||||||
|
password: str
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/signup")
|
||||||
|
async def sign_up(request: SignUpRequest):
|
||||||
|
"""Sign up a new user"""
|
||||||
|
supabase = get_supabase()
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = supabase.auth.sign_up({"email": request.email, "password": request.password})
|
||||||
|
|
||||||
|
if result.user:
|
||||||
|
# Initialize user with free credits
|
||||||
|
supabase.table("users").insert(
|
||||||
|
{
|
||||||
|
"id": result.user.id,
|
||||||
|
"email": request.email,
|
||||||
|
"credits": 100, # Free tier credits
|
||||||
|
"subscription_tier": "free",
|
||||||
|
}
|
||||||
|
).execute()
|
||||||
|
|
||||||
|
return {"user": result.user, "session": result.session}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/signin")
|
||||||
|
async def sign_in(request: SignInRequest):
|
||||||
|
"""Sign in an existing user"""
|
||||||
|
supabase = get_supabase()
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = supabase.auth.sign_in_with_password(
|
||||||
|
{"email": request.email, "password": request.password}
|
||||||
|
)
|
||||||
|
return {"user": result.user, "session": result.session}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid credentials")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/signout")
|
||||||
|
async def sign_out():
|
||||||
|
"""Sign out the current user"""
|
||||||
|
supabase = get_supabase()
|
||||||
|
|
||||||
|
try:
|
||||||
|
supabase.auth.sign_out()
|
||||||
|
return {"message": "Signed out successfully"}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
206
wan-pwa/apps/api/routes/generation.py
Normal file
206
wan-pwa/apps/api/routes/generation.py
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
from fastapi import APIRouter, HTTPException, Depends, Header
|
||||||
|
from typing import Optional
|
||||||
|
from models.generation import (
|
||||||
|
TextToVideoRequest,
|
||||||
|
ImageToVideoRequest,
|
||||||
|
GenerationResponse,
|
||||||
|
GenerationStatus,
|
||||||
|
)
|
||||||
|
from services.replicate_service import ReplicateService
|
||||||
|
from services.credit_service import CreditService
|
||||||
|
from core.supabase import get_supabase
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_user_id(authorization: Optional[str] = Header(None)) -> str:
|
||||||
|
"""Extract user ID from authorization header"""
|
||||||
|
if not authorization or not authorization.startswith("Bearer "):
|
||||||
|
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||||
|
|
||||||
|
token = authorization.replace("Bearer ", "")
|
||||||
|
supabase = get_supabase()
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = supabase.auth.get_user(token)
|
||||||
|
return user.user.id
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid token")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/text-to-video", response_model=GenerationResponse)
|
||||||
|
async def generate_text_to_video(
|
||||||
|
request: TextToVideoRequest, user_id: str = Depends(get_user_id)
|
||||||
|
):
|
||||||
|
"""Generate video from text prompt"""
|
||||||
|
|
||||||
|
# Calculate credit cost
|
||||||
|
cost = CreditService.calculate_cost(request.model, request.resolution)
|
||||||
|
|
||||||
|
# Check and deduct credits
|
||||||
|
has_credits = await CreditService.deduct_credits(
|
||||||
|
user_id, cost, f"T2V generation: {request.model} @ {request.resolution}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not has_credits:
|
||||||
|
raise HTTPException(status_code=402, detail="Insufficient credits")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Start generation via Replicate
|
||||||
|
prediction_id = await ReplicateService.generate_text_to_video(
|
||||||
|
prompt=request.prompt,
|
||||||
|
negative_prompt=request.negative_prompt,
|
||||||
|
model=request.model,
|
||||||
|
resolution=request.resolution,
|
||||||
|
duration=request.duration,
|
||||||
|
seed=request.seed,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Store generation record in database
|
||||||
|
supabase = get_supabase()
|
||||||
|
generation = (
|
||||||
|
supabase.table("generations")
|
||||||
|
.insert(
|
||||||
|
{
|
||||||
|
"id": prediction_id,
|
||||||
|
"user_id": user_id,
|
||||||
|
"type": "text-to-video",
|
||||||
|
"prompt": request.prompt,
|
||||||
|
"model": request.model,
|
||||||
|
"resolution": request.resolution,
|
||||||
|
"status": "pending",
|
||||||
|
"credits_used": cost,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.execute()
|
||||||
|
)
|
||||||
|
|
||||||
|
return GenerationResponse(
|
||||||
|
id=prediction_id,
|
||||||
|
status="pending",
|
||||||
|
created_at=datetime.utcnow(),
|
||||||
|
credits_used=cost,
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Refund credits on error
|
||||||
|
await CreditService.add_credits(user_id, cost, "Refund: Generation failed")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/image-to-video", response_model=GenerationResponse)
|
||||||
|
async def generate_image_to_video(
|
||||||
|
request: ImageToVideoRequest, user_id: str = Depends(get_user_id)
|
||||||
|
):
|
||||||
|
"""Generate video from image"""
|
||||||
|
|
||||||
|
# Calculate credit cost
|
||||||
|
cost = CreditService.calculate_cost(request.model, request.resolution)
|
||||||
|
|
||||||
|
# Check and deduct credits
|
||||||
|
has_credits = await CreditService.deduct_credits(
|
||||||
|
user_id, cost, f"I2V generation: {request.model} @ {request.resolution}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not has_credits:
|
||||||
|
raise HTTPException(status_code=402, detail="Insufficient credits")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Start generation via Replicate
|
||||||
|
prediction_id = await ReplicateService.generate_image_to_video(
|
||||||
|
prompt=request.prompt,
|
||||||
|
image_url=request.image_url,
|
||||||
|
negative_prompt=request.negative_prompt,
|
||||||
|
resolution=request.resolution,
|
||||||
|
duration=request.duration,
|
||||||
|
seed=request.seed,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Store generation record
|
||||||
|
supabase = get_supabase()
|
||||||
|
supabase.table("generations").insert(
|
||||||
|
{
|
||||||
|
"id": prediction_id,
|
||||||
|
"user_id": user_id,
|
||||||
|
"type": "image-to-video",
|
||||||
|
"prompt": request.prompt,
|
||||||
|
"image_url": request.image_url,
|
||||||
|
"model": request.model,
|
||||||
|
"resolution": request.resolution,
|
||||||
|
"status": "pending",
|
||||||
|
"credits_used": cost,
|
||||||
|
}
|
||||||
|
).execute()
|
||||||
|
|
||||||
|
return GenerationResponse(
|
||||||
|
id=prediction_id,
|
||||||
|
status="pending",
|
||||||
|
created_at=datetime.utcnow(),
|
||||||
|
credits_used=cost,
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Refund credits on error
|
||||||
|
await CreditService.add_credits(user_id, cost, "Refund: Generation failed")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/status/{generation_id}", response_model=GenerationStatus)
|
||||||
|
async def get_generation_status(generation_id: str, user_id: str = Depends(get_user_id)):
|
||||||
|
"""Get status of a generation"""
|
||||||
|
|
||||||
|
# Verify ownership
|
||||||
|
supabase = get_supabase()
|
||||||
|
generation = (
|
||||||
|
supabase.table("generations")
|
||||||
|
.select("*")
|
||||||
|
.eq("id", generation_id)
|
||||||
|
.eq("user_id", user_id)
|
||||||
|
.single()
|
||||||
|
.execute()
|
||||||
|
)
|
||||||
|
|
||||||
|
if not generation.data:
|
||||||
|
raise HTTPException(status_code=404, detail="Generation not found")
|
||||||
|
|
||||||
|
# Get status from Replicate
|
||||||
|
status = await ReplicateService.get_prediction_status(generation_id)
|
||||||
|
|
||||||
|
# Update database with latest status
|
||||||
|
update_data = {"status": status["status"]}
|
||||||
|
if status["output"]:
|
||||||
|
update_data["video_url"] = status["output"]
|
||||||
|
if status["error"]:
|
||||||
|
update_data["error"] = status["error"]
|
||||||
|
|
||||||
|
supabase.table("generations").update(update_data).eq("id", generation_id).execute()
|
||||||
|
|
||||||
|
# Map status to progress percentage
|
||||||
|
progress_map = {"pending": 0, "processing": 50, "succeeded": 100, "failed": 0}
|
||||||
|
|
||||||
|
return GenerationStatus(
|
||||||
|
id=generation_id,
|
||||||
|
status=status["status"],
|
||||||
|
progress=progress_map.get(status["status"], 0),
|
||||||
|
video_url=status["output"],
|
||||||
|
error=status["error"],
|
||||||
|
logs=status["logs"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/history")
|
||||||
|
async def get_generation_history(user_id: str = Depends(get_user_id)):
|
||||||
|
"""Get user's generation history"""
|
||||||
|
|
||||||
|
supabase = get_supabase()
|
||||||
|
result = (
|
||||||
|
supabase.table("generations")
|
||||||
|
.select("*")
|
||||||
|
.eq("user_id", user_id)
|
||||||
|
.order("created_at", desc=True)
|
||||||
|
.limit(50)
|
||||||
|
.execute()
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"generations": result.data}
|
||||||
41
wan-pwa/apps/api/routes/users.py
Normal file
41
wan-pwa/apps/api/routes/users.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from routes.generation import get_user_id
|
||||||
|
from services.credit_service import CreditService
|
||||||
|
from core.supabase import get_supabase
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/me")
|
||||||
|
async def get_current_user(user_id: str = Depends(get_user_id)):
|
||||||
|
"""Get current user profile"""
|
||||||
|
supabase = get_supabase()
|
||||||
|
result = supabase.table("users").select("*").eq("id", user_id).single().execute()
|
||||||
|
|
||||||
|
if not result.data:
|
||||||
|
return {"error": "User not found"}
|
||||||
|
|
||||||
|
return result.data
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/credits")
|
||||||
|
async def get_user_credits(user_id: str = Depends(get_user_id)):
|
||||||
|
"""Get user's credit balance"""
|
||||||
|
credits = await CreditService.get_user_credits(user_id)
|
||||||
|
return {"credits": credits}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/transactions")
|
||||||
|
async def get_credit_transactions(user_id: str = Depends(get_user_id)):
|
||||||
|
"""Get user's credit transaction history"""
|
||||||
|
supabase = get_supabase()
|
||||||
|
result = (
|
||||||
|
supabase.table("credit_transactions")
|
||||||
|
.select("*")
|
||||||
|
.eq("user_id", user_id)
|
||||||
|
.order("created_at", desc=True)
|
||||||
|
.limit(50)
|
||||||
|
.execute()
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"transactions": result.data}
|
||||||
1
wan-pwa/apps/api/services/__init__.py
Normal file
1
wan-pwa/apps/api/services/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# Services package
|
||||||
102
wan-pwa/apps/api/services/credit_service.py
Normal file
102
wan-pwa/apps/api/services/credit_service.py
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
from core.supabase import get_supabase
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
class CreditService:
|
||||||
|
"""Service for managing user credits"""
|
||||||
|
|
||||||
|
# Credit costs for different operations
|
||||||
|
COSTS = {
|
||||||
|
"t2v-14B-480p": 10,
|
||||||
|
"t2v-14B-720p": 20,
|
||||||
|
"t2v-1.3B-480p": 5,
|
||||||
|
"i2v-14B-480p": 15,
|
||||||
|
"i2v-14B-720p": 25,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Free tier credits
|
||||||
|
FREE_TIER_CREDITS = 100
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_user_credits(user_id: str) -> int:
|
||||||
|
"""Get current credit balance for user"""
|
||||||
|
supabase = get_supabase()
|
||||||
|
result = supabase.table("users").select("credits").eq("id", user_id).single().execute()
|
||||||
|
return result.data.get("credits", 0) if result.data else 0
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def deduct_credits(user_id: str, amount: int, description: str) -> bool:
|
||||||
|
"""
|
||||||
|
Deduct credits from user account
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: User ID
|
||||||
|
amount: Amount of credits to deduct
|
||||||
|
description: Description of the transaction
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False if insufficient credits
|
||||||
|
"""
|
||||||
|
supabase = get_supabase()
|
||||||
|
|
||||||
|
# Get current balance
|
||||||
|
current_credits = await CreditService.get_user_credits(user_id)
|
||||||
|
|
||||||
|
if current_credits < amount:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Deduct credits
|
||||||
|
new_balance = current_credits - amount
|
||||||
|
supabase.table("users").update({"credits": new_balance}).eq("id", user_id).execute()
|
||||||
|
|
||||||
|
# Record transaction
|
||||||
|
supabase.table("credit_transactions").insert(
|
||||||
|
{
|
||||||
|
"user_id": user_id,
|
||||||
|
"amount": -amount,
|
||||||
|
"type": "deduction",
|
||||||
|
"description": description,
|
||||||
|
}
|
||||||
|
).execute()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def add_credits(user_id: str, amount: int, description: str) -> bool:
|
||||||
|
"""
|
||||||
|
Add credits to user account
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: User ID
|
||||||
|
amount: Amount of credits to add
|
||||||
|
description: Description of the transaction
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful
|
||||||
|
"""
|
||||||
|
supabase = get_supabase()
|
||||||
|
|
||||||
|
# Get current balance
|
||||||
|
current_credits = await CreditService.get_user_credits(user_id)
|
||||||
|
|
||||||
|
# Add credits
|
||||||
|
new_balance = current_credits + amount
|
||||||
|
supabase.table("users").update({"credits": new_balance}).eq("id", user_id).execute()
|
||||||
|
|
||||||
|
# Record transaction
|
||||||
|
supabase.table("credit_transactions").insert(
|
||||||
|
{
|
||||||
|
"user_id": user_id,
|
||||||
|
"amount": amount,
|
||||||
|
"type": "addition",
|
||||||
|
"description": description,
|
||||||
|
}
|
||||||
|
).execute()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def calculate_cost(model: str, resolution: str) -> int:
|
||||||
|
"""Calculate credit cost for a generation request"""
|
||||||
|
key = f"{model}-{resolution}"
|
||||||
|
return CreditService.COSTS.get(key, 10)
|
||||||
145
wan-pwa/apps/api/services/replicate_service.py
Normal file
145
wan-pwa/apps/api/services/replicate_service.py
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import replicate
|
||||||
|
from core.config import settings
|
||||||
|
from typing import Dict, Any, Optional
|
||||||
|
|
||||||
|
# Initialize Replicate client
|
||||||
|
replicate_client = replicate.Client(api_token=settings.REPLICATE_API_TOKEN)
|
||||||
|
|
||||||
|
|
||||||
|
class ReplicateService:
|
||||||
|
"""Service for interacting with Replicate API for video generation"""
|
||||||
|
|
||||||
|
# Model versions - these would be updated with actual Wan2.1 model versions
|
||||||
|
WAN_T2V_14B = "wan-ai/wan2.1-t2v-14b:latest"
|
||||||
|
WAN_T2V_1_3B = "wan-ai/wan2.1-t2v-1.3b:latest"
|
||||||
|
WAN_I2V_14B = "wan-ai/wan2.1-i2v-14b:latest"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def generate_text_to_video(
|
||||||
|
prompt: str,
|
||||||
|
negative_prompt: Optional[str] = None,
|
||||||
|
model: str = "t2v-14B",
|
||||||
|
resolution: str = "720p",
|
||||||
|
duration: int = 5,
|
||||||
|
seed: Optional[int] = None,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Generate video from text using Wan2.1 models via Replicate
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prompt: Text prompt for video generation
|
||||||
|
negative_prompt: Negative prompt to avoid certain features
|
||||||
|
model: Model version to use (t2v-14B or t2v-1.3B)
|
||||||
|
resolution: Video resolution (480p or 720p)
|
||||||
|
duration: Video duration in seconds
|
||||||
|
seed: Random seed for reproducibility
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Prediction ID from Replicate
|
||||||
|
"""
|
||||||
|
model_version = (
|
||||||
|
ReplicateService.WAN_T2V_14B if model == "t2v-14B" else ReplicateService.WAN_T2V_1_3B
|
||||||
|
)
|
||||||
|
|
||||||
|
# Map resolution to size
|
||||||
|
size_map = {"480p": "832*480", "720p": "1280*720"}
|
||||||
|
|
||||||
|
input_params = {
|
||||||
|
"prompt": prompt,
|
||||||
|
"size": size_map.get(resolution, "1280*720"),
|
||||||
|
"sample_steps": 50 if model == "t2v-14B" else 40,
|
||||||
|
}
|
||||||
|
|
||||||
|
if negative_prompt:
|
||||||
|
input_params["negative_prompt"] = negative_prompt
|
||||||
|
if seed is not None:
|
||||||
|
input_params["seed"] = seed
|
||||||
|
|
||||||
|
# Create prediction
|
||||||
|
prediction = replicate_client.predictions.create(
|
||||||
|
version=model_version, input=input_params
|
||||||
|
)
|
||||||
|
|
||||||
|
return prediction.id
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def generate_image_to_video(
|
||||||
|
prompt: str,
|
||||||
|
image_url: str,
|
||||||
|
negative_prompt: Optional[str] = None,
|
||||||
|
resolution: str = "720p",
|
||||||
|
duration: int = 5,
|
||||||
|
seed: Optional[int] = None,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Generate video from image using Wan2.1 I2V model via Replicate
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prompt: Text prompt for video generation
|
||||||
|
image_url: URL of the input image
|
||||||
|
negative_prompt: Negative prompt
|
||||||
|
resolution: Video resolution
|
||||||
|
duration: Video duration in seconds
|
||||||
|
seed: Random seed
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Prediction ID from Replicate
|
||||||
|
"""
|
||||||
|
size_map = {"480p": "832*480", "720p": "1280*720"}
|
||||||
|
|
||||||
|
input_params = {
|
||||||
|
"prompt": prompt,
|
||||||
|
"image": image_url,
|
||||||
|
"size": size_map.get(resolution, "1280*720"),
|
||||||
|
"sample_steps": 40,
|
||||||
|
}
|
||||||
|
|
||||||
|
if negative_prompt:
|
||||||
|
input_params["negative_prompt"] = negative_prompt
|
||||||
|
if seed is not None:
|
||||||
|
input_params["seed"] = seed
|
||||||
|
|
||||||
|
prediction = replicate_client.predictions.create(
|
||||||
|
version=ReplicateService.WAN_I2V_14B, input=input_params
|
||||||
|
)
|
||||||
|
|
||||||
|
return prediction.id
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_prediction_status(prediction_id: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Get the status of a Replicate prediction
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prediction_id: The prediction ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary containing status, output, and error information
|
||||||
|
"""
|
||||||
|
prediction = replicate_client.predictions.get(prediction_id)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": prediction.id,
|
||||||
|
"status": prediction.status,
|
||||||
|
"output": prediction.output,
|
||||||
|
"error": prediction.error,
|
||||||
|
"logs": prediction.logs,
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def cancel_prediction(prediction_id: str) -> bool:
|
||||||
|
"""
|
||||||
|
Cancel a running prediction
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prediction_id: The prediction ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if cancelled successfully
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
prediction = replicate_client.predictions.cancel(prediction_id)
|
||||||
|
return prediction.status == "canceled"
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error cancelling prediction: {e}")
|
||||||
|
return False
|
||||||
11
wan-pwa/apps/web/.env.example
Normal file
11
wan-pwa/apps/web/.env.example
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# Supabase
|
||||||
|
NEXT_PUBLIC_SUPABASE_URL=https://xxxxx.supabase.co
|
||||||
|
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGci...
|
||||||
|
SUPABASE_SERVICE_ROLE_KEY=eyJhbGci...
|
||||||
|
|
||||||
|
# API
|
||||||
|
NEXT_PUBLIC_API_URL=http://localhost:8000
|
||||||
|
|
||||||
|
# App
|
||||||
|
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
||||||
|
NODE_ENV=development
|
||||||
21
wan-pwa/apps/web/next.config.js
Normal file
21
wan-pwa/apps/web/next.config.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
const withPWA = require("next-pwa")({
|
||||||
|
dest: "public",
|
||||||
|
register: true,
|
||||||
|
skipWaiting: true,
|
||||||
|
disable: process.env.NODE_ENV === "development",
|
||||||
|
})
|
||||||
|
|
||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
reactStrictMode: true,
|
||||||
|
images: {
|
||||||
|
domains: ["supabase.co", "replicate.delivery"],
|
||||||
|
},
|
||||||
|
experimental: {
|
||||||
|
serverActions: {
|
||||||
|
bodySizeLimit: "10mb",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = withPWA(nextConfig)
|
||||||
48
wan-pwa/apps/web/package.json
Normal file
48
wan-pwa/apps/web/package.json
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"name": "@wan-pwa/web",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint",
|
||||||
|
"clean": "rm -rf .next node_modules"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@hookform/resolvers": "^3.9.1",
|
||||||
|
"@radix-ui/react-avatar": "^1.1.1",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.2",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||||
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
|
"@radix-ui/react-select": "^2.1.2",
|
||||||
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.1",
|
||||||
|
"@radix-ui/react-toast": "^1.2.2",
|
||||||
|
"@supabase/ssr": "^0.5.2",
|
||||||
|
"@supabase/supabase-js": "^2.46.1",
|
||||||
|
"class-variance-authority": "^0.7.0",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-react": "^0.454.0",
|
||||||
|
"next": "15.0.3",
|
||||||
|
"next-pwa": "^5.6.0",
|
||||||
|
"react": "^19.0.0-rc.0",
|
||||||
|
"react-dom": "^19.0.0-rc.0",
|
||||||
|
"react-hook-form": "^7.53.2",
|
||||||
|
"tailwind-merge": "^2.5.4",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"zod": "^3.23.8",
|
||||||
|
"zustand": "^5.0.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^18",
|
||||||
|
"@types/react-dom": "^18",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"eslint": "^8",
|
||||||
|
"eslint-config-next": "15.0.3",
|
||||||
|
"postcss": "^8",
|
||||||
|
"tailwindcss": "^3.4.1",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
wan-pwa/apps/web/postcss.config.js
Normal file
6
wan-pwa/apps/web/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
62
wan-pwa/apps/web/public/manifest.json
Normal file
62
wan-pwa/apps/web/public/manifest.json
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"name": "Wan2.1 PWA - AI Video Generation",
|
||||||
|
"short_name": "Wan2.1",
|
||||||
|
"description": "Generate stunning AI videos with Wan2.1 models",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#ffffff",
|
||||||
|
"theme_color": "#3b82f6",
|
||||||
|
"orientation": "portrait-primary",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-72x72.png",
|
||||||
|
"sizes": "72x72",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-96x96.png",
|
||||||
|
"sizes": "96x96",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-128x128.png",
|
||||||
|
"sizes": "128x128",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-144x144.png",
|
||||||
|
"sizes": "144x144",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-152x152.png",
|
||||||
|
"sizes": "152x152",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-384x384.png",
|
||||||
|
"sizes": "384x384",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"categories": ["productivity", "entertainment"],
|
||||||
|
"screenshots": []
|
||||||
|
}
|
||||||
59
wan-pwa/apps/web/src/app/globals.css
Normal file
59
wan-pwa/apps/web/src/app/globals.css
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 222.2 84% 4.9%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 222.2 84% 4.9%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 222.2 84% 4.9%;
|
||||||
|
--primary: 221.2 83.2% 53.3%;
|
||||||
|
--primary-foreground: 210 40% 98%;
|
||||||
|
--secondary: 210 40% 96.1%;
|
||||||
|
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--muted: 210 40% 96.1%;
|
||||||
|
--muted-foreground: 215.4 16.3% 46.9%;
|
||||||
|
--accent: 210 40% 96.1%;
|
||||||
|
--accent-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
--border: 214.3 31.8% 91.4%;
|
||||||
|
--input: 214.3 31.8% 91.4%;
|
||||||
|
--ring: 221.2 83.2% 53.3%;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 222.2 84% 4.9%;
|
||||||
|
--foreground: 210 40% 98%;
|
||||||
|
--card: 222.2 84% 4.9%;
|
||||||
|
--card-foreground: 210 40% 98%;
|
||||||
|
--popover: 222.2 84% 4.9%;
|
||||||
|
--popover-foreground: 210 40% 98%;
|
||||||
|
--primary: 217.2 91.2% 59.8%;
|
||||||
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--secondary: 217.2 32.6% 17.5%;
|
||||||
|
--secondary-foreground: 210 40% 98%;
|
||||||
|
--muted: 217.2 32.6% 17.5%;
|
||||||
|
--muted-foreground: 215 20.2% 65.1%;
|
||||||
|
--accent: 217.2 32.6% 17.5%;
|
||||||
|
--accent-foreground: 210 40% 98%;
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
--border: 217.2 32.6% 17.5%;
|
||||||
|
--input: 217.2 32.6% 17.5%;
|
||||||
|
--ring: 224.3 76.3% 48%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
38
wan-pwa/apps/web/src/app/layout.tsx
Normal file
38
wan-pwa/apps/web/src/app/layout.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import type { Metadata } from "next"
|
||||||
|
import { Inter } from "next/font/google"
|
||||||
|
import "./globals.css"
|
||||||
|
|
||||||
|
const inter = Inter({ subsets: ["latin"] })
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Wan2.1 PWA - AI Video Generation",
|
||||||
|
description: "Generate stunning AI videos with Wan2.1 models",
|
||||||
|
manifest: "/manifest.json",
|
||||||
|
themeColor: "#3b82f6",
|
||||||
|
appleWebApp: {
|
||||||
|
capable: true,
|
||||||
|
statusBarStyle: "default",
|
||||||
|
title: "Wan2.1",
|
||||||
|
},
|
||||||
|
viewport: {
|
||||||
|
width: "device-width",
|
||||||
|
initialScale: 1,
|
||||||
|
maximumScale: 1,
|
||||||
|
},
|
||||||
|
icons: {
|
||||||
|
icon: "/icons/icon-192x192.png",
|
||||||
|
apple: "/icons/icon-192x192.png",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body className={inter.className}>{children}</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
52
wan-pwa/apps/web/src/app/page.tsx
Normal file
52
wan-pwa/apps/web/src/app/page.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import Link from "next/link"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<main className="flex min-h-screen flex-col items-center justify-center p-8">
|
||||||
|
<div className="max-w-4xl text-center">
|
||||||
|
<h1 className="mb-4 text-6xl font-bold tracking-tight">
|
||||||
|
Wan2.1 <span className="text-primary">PWA</span>
|
||||||
|
</h1>
|
||||||
|
<p className="mb-8 text-xl text-muted-foreground">
|
||||||
|
AI-Powered Video Generation Platform
|
||||||
|
</p>
|
||||||
|
<p className="mb-12 text-lg">
|
||||||
|
Create stunning videos with Text-to-Video and Image-to-Video using state-of-the-art
|
||||||
|
Wan2.1 models. 50+ prompt templates, real-time generation tracking, and installable PWA
|
||||||
|
experience.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-4 sm:flex-row sm:justify-center">
|
||||||
|
<Button asChild size="lg">
|
||||||
|
<Link href="/auth/signup">Get Started</Link>
|
||||||
|
</Button>
|
||||||
|
<Button asChild variant="outline" size="lg">
|
||||||
|
<Link href="/dashboard">View Dashboard</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-16 grid gap-8 sm:grid-cols-3">
|
||||||
|
<div className="rounded-lg border p-6">
|
||||||
|
<h3 className="mb-2 text-lg font-semibold">Text-to-Video</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Generate videos from text prompts using advanced AI models
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-lg border p-6">
|
||||||
|
<h3 className="mb-2 text-lg font-semibold">Image-to-Video</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Animate images into dynamic video sequences
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-lg border p-6">
|
||||||
|
<h3 className="mb-2 text-lg font-semibold">50+ Templates</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Pre-built prompt templates for every creative need
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
47
wan-pwa/apps/web/src/components/ui/button.tsx
Normal file
47
wan-pwa/apps/web/src/components/ui/button.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||||
|
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||||
|
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-10 px-4 py-2",
|
||||||
|
sm: "h-9 rounded-md px-3",
|
||||||
|
lg: "h-11 rounded-md px-8",
|
||||||
|
icon: "h-10 w-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "button"
|
||||||
|
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Button.displayName = "Button"
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
49
wan-pwa/apps/web/src/components/ui/card.tsx
Normal file
49
wan-pwa/apps/web/src/components/ui/card.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Card.displayName = "Card"
|
||||||
|
|
||||||
|
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
|
||||||
|
)
|
||||||
|
)
|
||||||
|
CardHeader.displayName = "CardHeader"
|
||||||
|
|
||||||
|
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<h3 ref={ref} className={cn("text-2xl font-semibold leading-none tracking-tight", className)} {...props} />
|
||||||
|
)
|
||||||
|
)
|
||||||
|
CardTitle.displayName = "CardTitle"
|
||||||
|
|
||||||
|
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
||||||
|
)
|
||||||
|
)
|
||||||
|
CardDescription.displayName = "CardDescription"
|
||||||
|
|
||||||
|
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||||
|
({ className, ...props }, ref) => <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||||
|
)
|
||||||
|
CardContent.displayName = "CardContent"
|
||||||
|
|
||||||
|
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
|
||||||
|
)
|
||||||
|
)
|
||||||
|
CardFooter.displayName = "CardFooter"
|
||||||
|
|
||||||
|
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||||
24
wan-pwa/apps/web/src/components/ui/input.tsx
Normal file
24
wan-pwa/apps/web/src/components/ui/input.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||||
|
|
||||||
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
|
({ className, type, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
className={cn(
|
||||||
|
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Input.displayName = "Input"
|
||||||
|
|
||||||
|
export { Input }
|
||||||
388
wan-pwa/apps/web/src/lib/prompts/templates.ts
Normal file
388
wan-pwa/apps/web/src/lib/prompts/templates.ts
Normal file
@ -0,0 +1,388 @@
|
|||||||
|
export type PromptCategory =
|
||||||
|
| "cinematic"
|
||||||
|
| "animation"
|
||||||
|
| "realistic"
|
||||||
|
| "abstract"
|
||||||
|
| "nature"
|
||||||
|
| "people"
|
||||||
|
| "animals"
|
||||||
|
|
||||||
|
export interface PromptTemplate {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
category: PromptCategory
|
||||||
|
prompt: string
|
||||||
|
negativePrompt?: string
|
||||||
|
tags: string[]
|
||||||
|
featured?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const promptTemplates: PromptTemplate[] = [
|
||||||
|
// Cinematic
|
||||||
|
{
|
||||||
|
id: "cinematic-1",
|
||||||
|
title: "Epic Movie Scene",
|
||||||
|
category: "cinematic",
|
||||||
|
prompt:
|
||||||
|
"Cinematic wide shot of a lone hero standing on a cliff edge at sunset, dramatic lighting, volumetric fog, epic scale, film grain, shallow depth of field",
|
||||||
|
negativePrompt:
|
||||||
|
"blurry, low quality, static, overexposed, ugly, deformed, amateur, cartoon",
|
||||||
|
tags: ["cinematic", "hero", "sunset", "epic"],
|
||||||
|
featured: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cinematic-2",
|
||||||
|
title: "Noir Detective",
|
||||||
|
category: "cinematic",
|
||||||
|
prompt:
|
||||||
|
"Film noir style detective walking through rain-soaked city streets at night, neon lights reflecting on wet pavement, high contrast lighting, moody atmosphere",
|
||||||
|
negativePrompt: "bright colors, daytime, cheerful, low quality",
|
||||||
|
tags: ["noir", "detective", "rain", "night"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cinematic-3",
|
||||||
|
title: "Space Opera",
|
||||||
|
category: "cinematic",
|
||||||
|
prompt:
|
||||||
|
"Epic space battle with massive starships, laser beams, explosions, nebula in background, cinematic camera movement, lens flares",
|
||||||
|
negativePrompt: "static, low quality, cartoon, unrealistic",
|
||||||
|
tags: ["space", "battle", "sci-fi", "epic"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cinematic-4",
|
||||||
|
title: "Medieval Battle",
|
||||||
|
category: "cinematic",
|
||||||
|
prompt:
|
||||||
|
"Epic medieval battle scene, knights charging on horseback, castle siege, dramatic sky, volumetric dust and smoke, cinematic composition",
|
||||||
|
tags: ["medieval", "battle", "knights", "epic"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cinematic-5",
|
||||||
|
title: "Dystopian City",
|
||||||
|
category: "cinematic",
|
||||||
|
prompt:
|
||||||
|
"Futuristic dystopian cityscape, towering megastructures, neon signs, flying vehicles, rain, cyberpunk aesthetic, cinematic drone shot",
|
||||||
|
tags: ["cyberpunk", "dystopia", "future", "city"],
|
||||||
|
featured: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Animation
|
||||||
|
{
|
||||||
|
id: "animation-1",
|
||||||
|
title: "Pixar Style Character",
|
||||||
|
category: "animation",
|
||||||
|
prompt:
|
||||||
|
"Cute cartoon character in Pixar animation style, expressive eyes, dynamic pose, colorful environment, warm lighting, high quality 3D render",
|
||||||
|
negativePrompt: "realistic, photorealistic, ugly, deformed, low quality",
|
||||||
|
tags: ["pixar", "cute", "3d", "character"],
|
||||||
|
featured: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "animation-2",
|
||||||
|
title: "Studio Ghibli Landscape",
|
||||||
|
category: "animation",
|
||||||
|
prompt:
|
||||||
|
"Beautiful countryside landscape in Studio Ghibli animation style, rolling hills, traditional Japanese houses, cherry blossoms, peaceful atmosphere",
|
||||||
|
tags: ["ghibli", "landscape", "peaceful", "japan"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "animation-3",
|
||||||
|
title: "Cartoon Animals",
|
||||||
|
category: "animation",
|
||||||
|
prompt:
|
||||||
|
"Adorable cartoon animals playing in a magical forest, vibrant colors, playful animation, Disney-style, whimsical atmosphere",
|
||||||
|
tags: ["animals", "cartoon", "forest", "playful"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "animation-4",
|
||||||
|
title: "Anime Action",
|
||||||
|
category: "animation",
|
||||||
|
prompt:
|
||||||
|
"Dynamic anime-style action sequence, character with special powers, energy effects, speed lines, dramatic camera angles, vibrant colors",
|
||||||
|
tags: ["anime", "action", "powers", "dynamic"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "animation-5",
|
||||||
|
title: "Claymation Scene",
|
||||||
|
category: "animation",
|
||||||
|
prompt:
|
||||||
|
"Charming claymation-style scene with textured characters, stop-motion aesthetic, warm lighting, cozy atmosphere, handcrafted feel",
|
||||||
|
tags: ["claymation", "stop-motion", "handcrafted", "cozy"],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Realistic
|
||||||
|
{
|
||||||
|
id: "realistic-1",
|
||||||
|
title: "Nature Documentary",
|
||||||
|
category: "realistic",
|
||||||
|
prompt:
|
||||||
|
"National Geographic style wildlife footage, majestic lion on African savanna at golden hour, cinematic camera work, 4K quality, natural lighting",
|
||||||
|
negativePrompt: "cartoon, animated, low quality, static",
|
||||||
|
tags: ["wildlife", "nature", "documentary", "africa"],
|
||||||
|
featured: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "realistic-2",
|
||||||
|
title: "Urban Photography",
|
||||||
|
category: "realistic",
|
||||||
|
prompt:
|
||||||
|
"Photorealistic urban street scene, busy city intersection, people walking, cars moving, realistic lighting and shadows, documentary style",
|
||||||
|
tags: ["urban", "street", "documentary", "people"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "realistic-3",
|
||||||
|
title: "Portrait Cinematic",
|
||||||
|
category: "realistic",
|
||||||
|
prompt:
|
||||||
|
"Cinematic portrait of a person, shallow depth of field, professional lighting, emotional expression, film grain, anamorphic lens look",
|
||||||
|
tags: ["portrait", "cinematic", "emotional", "professional"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "realistic-4",
|
||||||
|
title: "Architectural Tour",
|
||||||
|
category: "realistic",
|
||||||
|
prompt:
|
||||||
|
"Architectural visualization, modern building exterior, smooth camera movement, golden hour lighting, photorealistic materials and textures",
|
||||||
|
tags: ["architecture", "building", "modern", "professional"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "realistic-5",
|
||||||
|
title: "Underwater World",
|
||||||
|
category: "realistic",
|
||||||
|
prompt:
|
||||||
|
"Photorealistic underwater scene, colorful coral reef, tropical fish swimming, sunlight rays penetrating water, documentary quality",
|
||||||
|
tags: ["underwater", "ocean", "reef", "documentary"],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Abstract
|
||||||
|
{
|
||||||
|
id: "abstract-1",
|
||||||
|
title: "Liquid Art",
|
||||||
|
category: "abstract",
|
||||||
|
prompt:
|
||||||
|
"Abstract liquid art, flowing colorful fluids, paint mixing, organic shapes, macro photography style, vibrant colors, smooth motion",
|
||||||
|
tags: ["abstract", "liquid", "colorful", "organic"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "abstract-2",
|
||||||
|
title: "Geometric Motion",
|
||||||
|
category: "abstract",
|
||||||
|
prompt:
|
||||||
|
"Abstract geometric shapes morphing and rotating, neon colors, symmetrical patterns, mathematical precision, hypnotic motion",
|
||||||
|
tags: ["geometric", "abstract", "neon", "patterns"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "abstract-3",
|
||||||
|
title: "Particle System",
|
||||||
|
category: "abstract",
|
||||||
|
prompt:
|
||||||
|
"Abstract particle system, millions of particles forming complex patterns, flowing and dissolving, ethereal atmosphere, dark background",
|
||||||
|
tags: ["particles", "abstract", "ethereal", "complex"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "abstract-4",
|
||||||
|
title: "Fractal Zoom",
|
||||||
|
category: "abstract",
|
||||||
|
prompt:
|
||||||
|
"Infinite fractal zoom, mathematical patterns, psychedelic colors, recursive geometry, mesmerizing motion, high detail",
|
||||||
|
tags: ["fractal", "mathematical", "psychedelic", "infinite"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "abstract-5",
|
||||||
|
title: "Digital Glitch",
|
||||||
|
category: "abstract",
|
||||||
|
prompt:
|
||||||
|
"Digital glitch art aesthetic, datamoshing effects, chromatic aberration, pixel sorting, cyberpunk colors, corrupted data visualization",
|
||||||
|
tags: ["glitch", "digital", "cyberpunk", "corrupted"],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Nature
|
||||||
|
{
|
||||||
|
id: "nature-1",
|
||||||
|
title: "Mountain Landscape",
|
||||||
|
category: "nature",
|
||||||
|
prompt:
|
||||||
|
"Majestic mountain landscape with snow-capped peaks, alpine meadow with wildflowers, flowing stream, dramatic clouds, sunrise lighting",
|
||||||
|
tags: ["mountain", "landscape", "alpine", "sunrise"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "nature-2",
|
||||||
|
title: "Ocean Waves",
|
||||||
|
category: "nature",
|
||||||
|
prompt:
|
||||||
|
"Powerful ocean waves crashing on rocky shore, sea spray, dramatic sky, slow motion, natural beauty, coastal scenery",
|
||||||
|
tags: ["ocean", "waves", "coastal", "dramatic"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "nature-3",
|
||||||
|
title: "Forest Path",
|
||||||
|
category: "nature",
|
||||||
|
prompt:
|
||||||
|
"Peaceful forest path with sunlight filtering through trees, dappled light, morning mist, lush vegetation, serene atmosphere",
|
||||||
|
tags: ["forest", "path", "peaceful", "sunlight"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "nature-4",
|
||||||
|
title: "Desert Sunset",
|
||||||
|
category: "nature",
|
||||||
|
prompt:
|
||||||
|
"Vast desert landscape at sunset, sand dunes, warm golden light, long shadows, clear sky transitioning to night, peaceful solitude",
|
||||||
|
tags: ["desert", "sunset", "dunes", "peaceful"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "nature-5",
|
||||||
|
title: "Waterfall Paradise",
|
||||||
|
category: "nature",
|
||||||
|
prompt:
|
||||||
|
"Stunning tropical waterfall, crystal clear water, lush green vegetation, rainbow in mist, natural pool, paradise setting",
|
||||||
|
tags: ["waterfall", "tropical", "paradise", "rainbow"],
|
||||||
|
},
|
||||||
|
|
||||||
|
// People
|
||||||
|
{
|
||||||
|
id: "people-1",
|
||||||
|
title: "Dance Performance",
|
||||||
|
category: "people",
|
||||||
|
prompt:
|
||||||
|
"Professional dancer performing contemporary dance, fluid movements, dramatic lighting, stage performance, emotional expression, elegant choreography",
|
||||||
|
tags: ["dance", "performance", "elegant", "artistic"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "people-2",
|
||||||
|
title: "Street Musician",
|
||||||
|
category: "people",
|
||||||
|
prompt:
|
||||||
|
"Street musician playing guitar on urban street corner, passersby, natural lighting, documentary style, authentic moment, city atmosphere",
|
||||||
|
tags: ["music", "street", "urban", "documentary"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "people-3",
|
||||||
|
title: "Chef at Work",
|
||||||
|
category: "people",
|
||||||
|
prompt:
|
||||||
|
"Professional chef preparing gourmet dish, kitchen environment, precise movements, steam and sizzling, cinematic close-up shots, culinary artistry",
|
||||||
|
tags: ["chef", "cooking", "culinary", "professional"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "people-4",
|
||||||
|
title: "Athlete Training",
|
||||||
|
category: "people",
|
||||||
|
prompt:
|
||||||
|
"Athlete training intensely, gym environment, dynamic movements, sweat details, determination, motivational atmosphere, dramatic lighting",
|
||||||
|
tags: ["athlete", "training", "fitness", "motivation"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "people-5",
|
||||||
|
title: "Fashion Runway",
|
||||||
|
category: "people",
|
||||||
|
prompt:
|
||||||
|
"Fashion model walking down runway, haute couture clothing, professional lighting, confident stride, fashion show atmosphere, elegant presentation",
|
||||||
|
tags: ["fashion", "runway", "model", "elegant"],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Animals
|
||||||
|
{
|
||||||
|
id: "animals-1",
|
||||||
|
title: "Bird in Flight",
|
||||||
|
category: "animals",
|
||||||
|
prompt:
|
||||||
|
"Majestic eagle soaring through mountain valley, wings spread, slow motion, natural habitat, cloudy sky, wildlife cinematography",
|
||||||
|
tags: ["bird", "eagle", "flight", "wildlife"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "animals-2",
|
||||||
|
title: "Playful Dolphins",
|
||||||
|
category: "animals",
|
||||||
|
prompt:
|
||||||
|
"Pod of dolphins jumping and playing in ocean waves, underwater and above water shots, sunlight, joyful energy, marine life",
|
||||||
|
tags: ["dolphins", "ocean", "playful", "marine"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "animals-3",
|
||||||
|
title: "Tiger Hunt",
|
||||||
|
category: "animals",
|
||||||
|
prompt:
|
||||||
|
"Bengal tiger stalking through jungle, intense focus, powerful movements, dappled sunlight through canopy, predator instincts, wildlife drama",
|
||||||
|
tags: ["tiger", "jungle", "predator", "wildlife"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "animals-4",
|
||||||
|
title: "Butterfly Metamorphosis",
|
||||||
|
category: "animals",
|
||||||
|
prompt:
|
||||||
|
"Time-lapse of butterfly emerging from chrysalis, delicate wings unfurling, macro detail, natural beauty, transformation process",
|
||||||
|
tags: ["butterfly", "metamorphosis", "macro", "nature"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "animals-5",
|
||||||
|
title: "Wolf Pack",
|
||||||
|
category: "animals",
|
||||||
|
prompt:
|
||||||
|
"Wolf pack moving through snowy forest, coordinated movement, winter landscape, misty breath, wild beauty, pack dynamics",
|
||||||
|
tags: ["wolf", "pack", "winter", "forest"],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Additional templates to reach 50+
|
||||||
|
{
|
||||||
|
id: "cinematic-6",
|
||||||
|
title: "Car Chase",
|
||||||
|
category: "cinematic",
|
||||||
|
prompt:
|
||||||
|
"High-speed car chase through city streets, dynamic camera angles, motion blur, tire smoke, dramatic pursuit, action movie style",
|
||||||
|
tags: ["cars", "chase", "action", "speed"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "animation-6",
|
||||||
|
title: "Magical Transformation",
|
||||||
|
category: "animation",
|
||||||
|
prompt:
|
||||||
|
"Magical girl transformation sequence, sparkles and light effects, anime style, dynamic poses, colorful energy, enchanting atmosphere",
|
||||||
|
tags: ["magic", "transformation", "anime", "sparkles"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "realistic-6",
|
||||||
|
title: "Concert Performance",
|
||||||
|
category: "realistic",
|
||||||
|
prompt:
|
||||||
|
"Live concert performance, crowd energy, stage lights, musicians performing, photorealistic, dynamic camera work, electric atmosphere",
|
||||||
|
tags: ["concert", "music", "performance", "crowd"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "abstract-7",
|
||||||
|
title: "Smoke Art",
|
||||||
|
category: "abstract",
|
||||||
|
prompt:
|
||||||
|
"Colorful smoke wisps and tendrils, black background, fluid motion, ethereal patterns, vibrant colors mixing, hypnotic movement",
|
||||||
|
tags: ["smoke", "abstract", "colorful", "ethereal"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "nature-6",
|
||||||
|
title: "Northern Lights",
|
||||||
|
category: "nature",
|
||||||
|
prompt:
|
||||||
|
"Aurora borealis dancing across night sky, green and purple lights, snowy landscape below, stars visible, magical natural phenomenon",
|
||||||
|
tags: ["aurora", "northern lights", "night", "magical"],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export function getTemplatesByCategory(category: PromptCategory): PromptTemplate[] {
|
||||||
|
return promptTemplates.filter((t) => t.category === category)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFeaturedTemplates(): PromptTemplate[] {
|
||||||
|
return promptTemplates.filter((t) => t.featured)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTemplateById(id: string): PromptTemplate | undefined {
|
||||||
|
return promptTemplates.find((t) => t.id === id)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function searchTemplates(query: string): PromptTemplate[] {
|
||||||
|
const lowerQuery = query.toLowerCase()
|
||||||
|
return promptTemplates.filter(
|
||||||
|
(t) =>
|
||||||
|
t.title.toLowerCase().includes(lowerQuery) ||
|
||||||
|
t.prompt.toLowerCase().includes(lowerQuery) ||
|
||||||
|
t.tags.some((tag) => tag.toLowerCase().includes(lowerQuery))
|
||||||
|
)
|
||||||
|
}
|
||||||
8
wan-pwa/apps/web/src/lib/supabase/client.ts
Normal file
8
wan-pwa/apps/web/src/lib/supabase/client.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { createBrowserClient } from "@supabase/ssr"
|
||||||
|
|
||||||
|
export function createClient() {
|
||||||
|
return createBrowserClient(
|
||||||
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||||
|
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||||
|
)
|
||||||
|
}
|
||||||
32
wan-pwa/apps/web/src/lib/supabase/server.ts
Normal file
32
wan-pwa/apps/web/src/lib/supabase/server.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { createServerClient, type CookieOptions } from "@supabase/ssr"
|
||||||
|
import { cookies } from "next/headers"
|
||||||
|
|
||||||
|
export async function createClient() {
|
||||||
|
const cookieStore = await cookies()
|
||||||
|
|
||||||
|
return createServerClient(
|
||||||
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||||
|
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||||
|
{
|
||||||
|
cookies: {
|
||||||
|
get(name: string) {
|
||||||
|
return cookieStore.get(name)?.value
|
||||||
|
},
|
||||||
|
set(name: string, value: string, options: CookieOptions) {
|
||||||
|
try {
|
||||||
|
cookieStore.set({ name, value, ...options })
|
||||||
|
} catch (error) {
|
||||||
|
// Handle cookie setting errors in server components
|
||||||
|
}
|
||||||
|
},
|
||||||
|
remove(name: string, options: CookieOptions) {
|
||||||
|
try {
|
||||||
|
cookieStore.set({ name, value: "", ...options })
|
||||||
|
} catch (error) {
|
||||||
|
// Handle cookie removal errors in server components
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
25
wan-pwa/apps/web/src/lib/utils.ts
Normal file
25
wan-pwa/apps/web/src/lib/utils.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { type ClassValue, clsx } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDate(date: Date | string): string {
|
||||||
|
const d = typeof date === "string" ? new Date(date) : date
|
||||||
|
return new Intl.DateTimeFormat("en-US", {
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
year: "numeric",
|
||||||
|
}).format(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDuration(seconds: number): string {
|
||||||
|
const mins = Math.floor(seconds / 60)
|
||||||
|
const secs = seconds % 60
|
||||||
|
return `${mins}:${secs.toString().padStart(2, "0")}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatCredits(credits: number): string {
|
||||||
|
return new Intl.NumberFormat("en-US").format(credits)
|
||||||
|
}
|
||||||
54
wan-pwa/apps/web/tailwind.config.js
Normal file
54
wan-pwa/apps/web/tailwind.config.js
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
darkMode: ["class"],
|
||||||
|
content: [
|
||||||
|
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
border: "hsl(var(--border))",
|
||||||
|
input: "hsl(var(--input))",
|
||||||
|
ring: "hsl(var(--ring))",
|
||||||
|
background: "hsl(var(--background))",
|
||||||
|
foreground: "hsl(var(--foreground))",
|
||||||
|
primary: {
|
||||||
|
DEFAULT: "hsl(var(--primary))",
|
||||||
|
foreground: "hsl(var(--primary-foreground))",
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: "hsl(var(--secondary))",
|
||||||
|
foreground: "hsl(var(--secondary-foreground))",
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: "hsl(var(--destructive))",
|
||||||
|
foreground: "hsl(var(--destructive-foreground))",
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: "hsl(var(--muted))",
|
||||||
|
foreground: "hsl(var(--muted-foreground))",
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: "hsl(var(--accent))",
|
||||||
|
foreground: "hsl(var(--accent-foreground))",
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: "hsl(var(--popover))",
|
||||||
|
foreground: "hsl(var(--popover-foreground))",
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
DEFAULT: "hsl(var(--card))",
|
||||||
|
foreground: "hsl(var(--card-foreground))",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
lg: "var(--radius)",
|
||||||
|
md: "calc(var(--radius) - 2px)",
|
||||||
|
sm: "calc(var(--radius) - 4px)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [require("tailwindcss-animate")],
|
||||||
|
}
|
||||||
27
wan-pwa/apps/web/tsconfig.json
Normal file
27
wan-pwa/apps/web/tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
32
wan-pwa/package.json
Normal file
32
wan-pwa/package.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "wan-pwa",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Wan2.1 PWA - AI Video Generation Platform",
|
||||||
|
"workspaces": [
|
||||||
|
"apps/*",
|
||||||
|
"packages/*"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"dev": "turbo run dev",
|
||||||
|
"build": "turbo run build",
|
||||||
|
"start": "turbo run start",
|
||||||
|
"lint": "turbo run lint",
|
||||||
|
"test": "turbo run test",
|
||||||
|
"clean": "turbo run clean && rm -rf node_modules",
|
||||||
|
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,css,md}\"",
|
||||||
|
"db:migrate": "cd packages/db && npm run migrate",
|
||||||
|
"db:seed": "cd packages/db && npm run seed"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@turbo/gen": "^2.3.0",
|
||||||
|
"prettier": "^3.3.3",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.6.8",
|
||||||
|
"turbo": "^2.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0",
|
||||||
|
"npm": ">=9.0.0"
|
||||||
|
},
|
||||||
|
"packageManager": "npm@10.2.3"
|
||||||
|
}
|
||||||
60
wan-pwa/packages/db/README.md
Normal file
60
wan-pwa/packages/db/README.md
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
# Database Package
|
||||||
|
|
||||||
|
This package contains database schema and migrations for the Wan2.1 PWA.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. Go to your Supabase dashboard
|
||||||
|
2. Navigate to SQL Editor
|
||||||
|
3. Create a new query
|
||||||
|
4. Copy the contents of `migrations/001_initial_schema.sql`
|
||||||
|
5. Run the query
|
||||||
|
|
||||||
|
## Schema
|
||||||
|
|
||||||
|
### Tables
|
||||||
|
|
||||||
|
#### users
|
||||||
|
- Stores user profile data
|
||||||
|
- Extends Supabase auth.users
|
||||||
|
- Tracks credits and subscription tier
|
||||||
|
|
||||||
|
#### generations
|
||||||
|
- Stores video generation requests and results
|
||||||
|
- Links to users and tracks status
|
||||||
|
- Stores prompts, settings, and output URLs
|
||||||
|
|
||||||
|
#### credit_transactions
|
||||||
|
- Tracks all credit additions and deductions
|
||||||
|
- Provides audit trail for user credits
|
||||||
|
|
||||||
|
### Storage
|
||||||
|
|
||||||
|
#### images bucket
|
||||||
|
- Stores uploaded images for Image-to-Video generation
|
||||||
|
- Publicly accessible
|
||||||
|
- Organized by user ID
|
||||||
|
|
||||||
|
## Row Level Security (RLS)
|
||||||
|
|
||||||
|
All tables have RLS enabled to ensure users can only access their own data:
|
||||||
|
|
||||||
|
- Users can read/update their own profile
|
||||||
|
- Users can view/create their own generations
|
||||||
|
- Users can view their own transactions
|
||||||
|
|
||||||
|
## Migrations
|
||||||
|
|
||||||
|
Migrations should be run in order:
|
||||||
|
1. `001_initial_schema.sql` - Core schema
|
||||||
|
2. `002_seed_data.sql` - Optional seed data
|
||||||
|
|
||||||
|
## Indexes
|
||||||
|
|
||||||
|
Indexes are created on:
|
||||||
|
- `generations.user_id`
|
||||||
|
- `generations.created_at`
|
||||||
|
- `credit_transactions.user_id`
|
||||||
|
- `credit_transactions.created_at`
|
||||||
|
|
||||||
|
These optimize common queries for user data and history.
|
||||||
119
wan-pwa/packages/db/migrations/001_initial_schema.sql
Normal file
119
wan-pwa/packages/db/migrations/001_initial_schema.sql
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
-- Enable UUID extension
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||||
|
|
||||||
|
-- Users table (extends Supabase auth.users)
|
||||||
|
CREATE TABLE IF NOT EXISTS public.users (
|
||||||
|
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||||
|
email TEXT NOT NULL UNIQUE,
|
||||||
|
credits INTEGER NOT NULL DEFAULT 100,
|
||||||
|
subscription_tier TEXT NOT NULL DEFAULT 'free',
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Enable RLS
|
||||||
|
ALTER TABLE public.users ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- Users can only read their own data
|
||||||
|
CREATE POLICY "Users can view own data" ON public.users
|
||||||
|
FOR SELECT USING (auth.uid() = id);
|
||||||
|
|
||||||
|
-- Users can update their own data
|
||||||
|
CREATE POLICY "Users can update own data" ON public.users
|
||||||
|
FOR UPDATE USING (auth.uid() = id);
|
||||||
|
|
||||||
|
-- Generations table
|
||||||
|
CREATE TABLE IF NOT EXISTS public.generations (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE,
|
||||||
|
type TEXT NOT NULL CHECK (type IN ('text-to-video', 'image-to-video')),
|
||||||
|
prompt TEXT NOT NULL,
|
||||||
|
negative_prompt TEXT,
|
||||||
|
image_url TEXT,
|
||||||
|
model TEXT NOT NULL,
|
||||||
|
resolution TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'completed', 'failed')),
|
||||||
|
video_url TEXT,
|
||||||
|
error TEXT,
|
||||||
|
credits_used INTEGER NOT NULL,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
completed_at TIMESTAMP WITH TIME ZONE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Enable RLS
|
||||||
|
ALTER TABLE public.generations ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- Users can only view their own generations
|
||||||
|
CREATE POLICY "Users can view own generations" ON public.generations
|
||||||
|
FOR SELECT USING (auth.uid() = user_id);
|
||||||
|
|
||||||
|
-- Users can insert their own generations
|
||||||
|
CREATE POLICY "Users can create own generations" ON public.generations
|
||||||
|
FOR INSERT WITH CHECK (auth.uid() = user_id);
|
||||||
|
|
||||||
|
-- Credit transactions table
|
||||||
|
CREATE TABLE IF NOT EXISTS public.credit_transactions (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE,
|
||||||
|
amount INTEGER NOT NULL,
|
||||||
|
type TEXT NOT NULL CHECK (type IN ('addition', 'deduction', 'refund')),
|
||||||
|
description TEXT,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Enable RLS
|
||||||
|
ALTER TABLE public.credit_transactions ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- Users can only view their own transactions
|
||||||
|
CREATE POLICY "Users can view own transactions" ON public.credit_transactions
|
||||||
|
FOR SELECT USING (auth.uid() = user_id);
|
||||||
|
|
||||||
|
-- Create indexes for better performance
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_generations_user_id ON public.generations(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_generations_created_at ON public.generations(created_at DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_credit_transactions_user_id ON public.credit_transactions(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_credit_transactions_created_at ON public.credit_transactions(created_at DESC);
|
||||||
|
|
||||||
|
-- Function to update updated_at timestamp
|
||||||
|
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.updated_at = NOW();
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ language 'plpgsql';
|
||||||
|
|
||||||
|
-- Trigger to auto-update updated_at
|
||||||
|
CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON public.users
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||||
|
|
||||||
|
-- Function to create user profile on signup
|
||||||
|
CREATE OR REPLACE FUNCTION public.handle_new_user()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO public.users (id, email, credits, subscription_tier)
|
||||||
|
VALUES (NEW.id, NEW.email, 100, 'free')
|
||||||
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||||
|
|
||||||
|
-- Trigger to create user profile on auth signup
|
||||||
|
CREATE TRIGGER on_auth_user_created
|
||||||
|
AFTER INSERT ON auth.users
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();
|
||||||
|
|
||||||
|
-- Storage bucket for uploaded images (for I2V)
|
||||||
|
INSERT INTO storage.buckets (id, name, public)
|
||||||
|
VALUES ('images', 'images', true)
|
||||||
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- Storage policy for images
|
||||||
|
CREATE POLICY "Users can upload own images" ON storage.objects
|
||||||
|
FOR INSERT WITH CHECK (
|
||||||
|
bucket_id = 'images' AND
|
||||||
|
auth.uid()::text = (storage.foldername(name))[1]
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE POLICY "Images are publicly accessible" ON storage.objects
|
||||||
|
FOR SELECT USING (bucket_id = 'images');
|
||||||
10
wan-pwa/packages/db/migrations/002_seed_data.sql
Normal file
10
wan-pwa/packages/db/migrations/002_seed_data.sql
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
-- Seed data for testing (optional)
|
||||||
|
|
||||||
|
-- This is example seed data
|
||||||
|
-- In production, users will sign up and get credits automatically
|
||||||
|
|
||||||
|
-- Example: Add bonus credits to specific users
|
||||||
|
-- UPDATE public.users SET credits = credits + 500 WHERE email = 'test@example.com';
|
||||||
|
|
||||||
|
-- You can also add example generations for testing
|
||||||
|
-- (This would typically be done through the API)
|
||||||
10
wan-pwa/packages/db/package.json
Normal file
10
wan-pwa/packages/db/package.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "@wan-pwa/db",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Database schema and migrations",
|
||||||
|
"scripts": {
|
||||||
|
"migrate": "echo 'Run migrations in Supabase dashboard'",
|
||||||
|
"seed": "echo 'Run seed data in Supabase dashboard'"
|
||||||
|
}
|
||||||
|
}
|
||||||
23
wan-pwa/turbo.json
Normal file
23
wan-pwa/turbo.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://turbo.build/schema.json",
|
||||||
|
"globalDependencies": ["**/.env.*local"],
|
||||||
|
"pipeline": {
|
||||||
|
"build": {
|
||||||
|
"dependsOn": ["^build"],
|
||||||
|
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
|
||||||
|
},
|
||||||
|
"dev": {
|
||||||
|
"cache": false,
|
||||||
|
"persistent": true
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"dependsOn": ["^lint"]
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"dependsOn": ["^test"]
|
||||||
|
},
|
||||||
|
"clean": {
|
||||||
|
"cache": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user