{"id":50607505,"url":"https://github.com/luis-codex/qwen-reader","last_synced_at":"2026-06-06T00:30:18.701Z","repository":{"id":353901524,"uuid":"1221341907","full_name":"luis-codex/qwen-reader","owner":"luis-codex","description":"Convert articles and documents to high-quality audio using Qwen3-TTS. Clean Architecture CLI with 90% test coverage.","archived":false,"fork":false,"pushed_at":"2026-04-26T06:28:00.000Z","size":6962,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-26T07:12:06.105Z","etag":null,"topics":["audio","clean-architecture","cli","markdown","python","qwen","text-to-speech","tts"],"latest_commit_sha":null,"homepage":"https://huggingface.co/Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/luis-codex.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-26T04:22:56.000Z","updated_at":"2026-04-26T05:49:15.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/luis-codex/qwen-reader","commit_stats":null,"previous_names":["luis-codex/qwen-reader"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/luis-codex/qwen-reader","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/luis-codex%2Fqwen-reader","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/luis-codex%2Fqwen-reader/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/luis-codex%2Fqwen-reader/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/luis-codex%2Fqwen-reader/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/luis-codex","download_url":"https://codeload.github.com/luis-codex/qwen-reader/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/luis-codex%2Fqwen-reader/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33965591,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-05T02:00:06.157Z","response_time":120,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["audio","clean-architecture","cli","markdown","python","qwen","text-to-speech","tts"],"created_at":"2026-06-06T00:30:18.044Z","updated_at":"2026-06-06T00:30:18.687Z","avatar_url":"https://github.com/luis-codex.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cimg width=\"14135\" height=\"6792\" alt=\"image\" src=\"https://github.com/user-attachments/assets/bd638001-e12b-49dd-8acf-2c33e83f427e\" /\u003e\n\u003cdiv align=\"center\"\u003e\n\n# 🎧 qwen-reader\n\n**Convert articles and documents to high-quality audio using Qwen3-TTS.**\n\n[![Python](https://img.shields.io/badge/python-3.10%2B-blue?logo=python\u0026logoColor=white)](https://python.org)\n[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)\n[![Tests](https://img.shields.io/badge/tests-82%20passed-brightgreen)](#-testing)\n[![Coverage](https://img.shields.io/badge/coverage-92%25-brightgreen)](#-coverage)\n[![Powered by](https://img.shields.io/badge/powered%20by-Qwen3--TTS-purple)](https://huggingface.co/Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice)\n\nTurn your markdown notes, articles, and text files into podcast-style audio you can listen to anywhere — powered by local AI inference on your GPU.\n\n\u003c/div\u003e\n\n---\n\n## ✨ Features\n\n- **10 languages** — Chinese, English, Japanese, Korean, German, French, Russian, Portuguese, Spanish, Italian\n- **9 premium voices** — Male and female speakers across languages, dialects, and age ranges\n- **Multi-format support** — `.md`, `.markdown`, `.txt`, `.rst`, `.text`\n- **Intelligent text cleaning** — Strips markdown syntax, code blocks, links, and front-matter before synthesis\n- **Batch processing** — Convert multiple files in a single command\n- **Chunked synthesis** — Splits long text at sentence boundaries for consistent quality\n- **Rich CLI output** — Progress bars, tables, and styled panels via [Rich](https://github.com/Textualize/rich)\n- **GPU accelerated** — Runs on CUDA with auto-detection fallback to CPU\n\n## 📦 Installation\n\n### Prerequisites\n\n- Python 3.10+\n- NVIDIA GPU with CUDA support (recommended) or CPU\n- [uv](https://docs.astral.sh/uv/) (recommended) or pip\n\n### Setup\n\n```bash\ngit clone https://github.com/luis-codex/qwen-reader.git\ncd qwen-reader\n\n# Create virtual environment and install\nuv venv\nuv pip install -e .\n\n# Install PyTorch with CUDA support (adjust cu128 to your CUDA version)\nuv pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu128\n\n# (Optional, Linux only) Install FlashAttention 2 for ~2x faster inference\npip install -U flash-attn --no-build-isolation\n```\n\n\u003e [!NOTE]\n\u003e The first run downloads the model (~3.5 GB) from HuggingFace. Subsequent runs load it from cache in ~30s.\n\n\u003e [!TIP]\n\u003e **FlashAttention 2** significantly reduces GPU memory usage and speeds up inference, but is only available on Linux with Ampere+ GPUs (RTX 30xx/40xx). Windows users can safely ignore the `flash-attn` warning — the manual PyTorch attention path works correctly, just slower.\n\n### Developer setup\n\n```bash\n# Install with dev dependencies (pytest, pytest-cov)\nuv pip install -e \".[dev]\"\n\n# Run the test suite\npython -m pytest\n```\n\n### Make it globally available\n\nAdd the virtual environment's `Scripts` (Windows) or `bin` (Linux/macOS) directory to your system `PATH`:\n\n```powershell\n# Windows (PowerShell) — run once\n$scriptsPath = \"$PWD\\.venv\\Scripts\"\n[Environment]::SetEnvironmentVariable(\"Path\", \"$([Environment]::GetEnvironmentVariable('Path', 'User'));$scriptsPath\", \"User\")\n```\n\n```bash\n# Linux / macOS — add to ~/.bashrc or ~/.zshrc\nexport PATH=\"/path/to/qwen-reader/.venv/bin:$PATH\"\n```\n\nThen open a new terminal and use `qwen-reader` from anywhere.\n\n## 🚀 Usage\n\n```\nUsage: qwen-reader [OPTIONS] COMMAND [ARGS]\n\nCommands:\n  read      Convert one or more files to audio\n  speak     Convert inline text to audio\n  speakers  List available TTS voices\n  list      List previously generated audio files\n```\n\n### Convert files to audio\n\n```bash\n# Single file\nqwen-reader read article.md\n\n# Multiple files at once\nqwen-reader read notes.txt report.md spec.rst\n\n# Choose a voice and language\nqwen-reader read article.md --speaker Ryan --lang English\n\n# Custom output directory\nqwen-reader read article.md --output-dir ./my-audio\n\n# Custom output filename\nqwen-reader read article.md --name my-podcast\n```\n\n### Speak inline text\n\n```bash\nqwen-reader speak \"Hello world, this is a test.\"\nqwen-reader speak \"Hola mundo, esto es una prueba.\" --lang Spanish --speaker Vivian\n```\n\n## 🌍 Audio Demos\n\nPre-generated audio samples across all 10 supported languages are available in the [`demos/`](demos/) folder.\nListen to them to hear the quality before setting up the tool yourself!\n\n| Sample | Language | Speaker | Voice |\n|--------|----------|---------|-------|\n| [`demo_english.wav`](demos/demo_english.wav) | 🇬🇧 English | Ryan | Dynamic male, strong rhythmic drive |\n| [`demo_spanish.wav`](demos/demo_spanish.wav) | 🇪🇸 Spanish | Vivian | Bright, edgy young female |\n| [`demo_chinese.wav`](demos/demo_chinese.wav) | 🇨🇳 Chinese | Serena | Warm, gentle young female |\n| [`demo_japanese.wav`](demos/demo_japanese.wav) | 🇯🇵 Japanese | Ono_Anna | Playful female, light nimble timbre |\n| [`demo_korean.wav`](demos/demo_korean.wav) | 🇰🇷 Korean | Sohee | Warm female, rich emotion |\n| [`demo_french.wav`](demos/demo_french.wav) | 🇫🇷 French | Aiden | Sunny American male |\n| [`demo_german.wav`](demos/demo_german.wav) | 🇩🇪 German | Aiden | Sunny American male |\n| [`demo_italian.wav`](demos/demo_italian.wav) | 🇮🇹 Italian | Vivian | Bright, edgy young female |\n| [`demo_portuguese.wav`](demos/demo_portuguese.wav) | 🇧🇷 Portuguese | Ryan | Dynamic male |\n| [`demo_russian.wav`](demos/demo_russian.wav) | 🇷🇺 Russian | Aiden | Sunny American male |\n\n\u003e To regenerate all demos: `pwsh scripts/generate_demos.ps1`\n\n### Explore voices\n\n```bash\nqwen-reader speakers\n```\n\n| Speaker | Voice Description | Native Language |\n|---------|-------------------|-----------------|\n| Vivian | Bright, slightly edgy young female | Chinese |\n| Serena | Warm, gentle young female | Chinese |\n| Uncle_Fu | Seasoned male, low mellow timbre | Chinese |\n| Dylan | Youthful Beijing male, clear natural | Chinese (Beijing Dialect) |\n| Eric | Lively Chengdu male, husky brightness | Chinese (Sichuan Dialect) |\n| Ryan | Dynamic male, strong rhythmic drive | English |\n| Aiden | Sunny American male, clear midrange | English |\n| Ono_Anna | Playful Japanese female, light nimble | Japanese |\n| Sohee | Warm Korean female, rich emotion | Korean |\n\n\u003e [!TIP]\n\u003e Each speaker can speak **any** of the 10 supported languages, but sounds best in their native language.\n\n### Browse generated files\n\n```bash\nqwen-reader list\n```\n\n```\n         📂 ~/qwen-reader-audio\n┌──────────────────────┬────────┬──────────────────┐\n│ File                 │   Size │ Modified         │\n├──────────────────────┼────────┼──────────────────┤\n│ 🔊 article.wav       │ 4.2 MB │ 2026-04-25 22:10 │\n│ 🔊 spoken_text.wav   │ 0.1 MB │ 2026-04-25 21:52 │\n├──────────────────────┼────────┼──────────────────┤\n│ 2 files              │ 4.3 MB │                  │\n└──────────────────────┴────────┴──────────────────┘\n```\n\n### Full option reference\n\n| Option         | Short | Default               | Description                                      |\n| -------------- | ----- | --------------------- | ------------------------------------------------ |\n| `--speaker`    | `-s`  | `Aiden`               | TTS voice to use                                 |\n| `--lang`       | `-l`  | `Auto`                | Language (Auto, English, Chinese, Spanish, etc.) |\n| `--instruct`   | `-i`  | _conversational_      | Style instruction for the TTS engine             |\n| `--output-dir` | `-o`  | `~/qwen-reader-audio` | Output directory                                 |\n| `--name`       | `-n`  | _filename stem_       | Custom output filename (without extension)       |\n| `--device`     | `-d`  | _auto-detected_       | Compute device (`cuda:0`, `cpu`) — auto-detects CUDA |\n| `--version`    | `-v`  | —                     | Show version                                     |\n| `--help`       | `-h`  | —                     | Show help                                        |\n\n## 🗂️ Supported file types\n\n| Extension          | Processing                                                              |\n| ------------------ | ----------------------------------------------------------------------- |\n| `.md`, `.markdown` | Strips YAML front-matter, code blocks, links, images, emphasis, headers |\n| `.rst`             | Strips directives, section underlines, inline markup                    |\n| `.txt`, `.text`    | Passed through as-is                                                    |\n\n## 🏗️ Architecture\n\nThis project follows **Clean Architecture** with strict layer boundaries and a unidirectional dependency rule.\n\n### Layer diagram\n\n```\n┌───────────────────────────────────────────────────────────┐\n│  Interface Layer                           cli/           │\n│  app.py · commands.py · options.py · rendering.py         │\n│  click + rich · args, output, exit codes                  │\n├───────────────────────────────────────────────────────────┤\n│  Use-Case Layer                    core/synthesis.py      │\n│  Orchestration · chunking → TTS → WAV assembly            │\n├──────────────────────┬────────────────────────────────────┤\n│  Domain Layer        │  Infrastructure Layer              │\n│  core/text.py        │  core/model.py                     │\n│  core/storage.py     │  Model lifecycle, GPU management   │\n│  Pure transforms,    │  torch, qwen_tts (deferred import) │\n│  file listing        │                                    │\n│  stdlib only         │                                    │\n└──────────────────────┴────────────────────────────────────┘\n```\n\n### Dependency rule\n\n```\nInterface → Use-Case → Domain\n                     → Infrastructure → External Systems\n```\n\nNo inner layer ever imports an outer layer. Core modules never call `print()`, `sys.exit()`, or import `click`/`rich`.\n\n### Project structure\n\n```\nqwen_reader/\n├── __init__.py              # Package version\n├── __main__.py              # python -m qwen_reader entry\n├── cli/\n│   ├── __init__.py          # Re-exports cli, main\n│   ├── app.py               # Click group, entry point, Windows UTF-8\n│   ├── commands.py          # read, speak, speakers, list commands\n│   ├── options.py           # Shared option decorators\n│   └── rendering.py         # Rich console, progress bars, result panel\n└── core/\n    ├── __init__.py          # Docstring only — no re-exports\n    ├── text.py              # Domain: text cleaning \u0026 chunking\n    ├── storage.py           # Domain: audio file listing \u0026 output dir\n    ├── model.py             # Infrastructure: lazy model singleton\n    └── synthesis.py         # Use-Case: audio generation orchestration\n\ntests/\n├── conftest.py              # FakeModel stub + shared fixtures\n├── test_text.py             # Domain layer — no mocks, stdlib only\n├── test_storage.py          # Domain layer — file listing, no mocks\n├── test_synthesis.py        # Use-Case layer — mocked infrastructure\n└── test_cli.py              # Interface layer — click CliRunner\n```\n\n### Layer contract\n\n| Layer              | Module(s)                                                             | Responsibility                              | Allowed deps                             | Forbidden                 |\n| ------------------ | --------------------------------------------------------------------- | ------------------------------------------- | ---------------------------------------- | ------------------------- |\n| **Interface**      | `cli/app.py`, `cli/commands.py`, `cli/options.py`, `cli/rendering.py` | Parse args, render output, map exit codes   | click, rich, Use-Case                    | torch, numpy, direct I/O  |\n| **Use-Case**       | `core/synthesis.py`                                                   | Orchestrate domain + infra into workflows   | Domain, Infrastructure, numpy, soundfile | click, rich, `print()`    |\n| **Domain**         | `core/text.py`, `core/storage.py`                                     | Pure text transforms, file listing          | **stdlib only** (`re`, `os`, `time`)     | Any third-party package   |\n| **Infrastructure** | `core/model.py`                                                       | External system lifecycle (model load, GPU) | torch, qwen_tts, stdlib                  | click, rich, domain logic |\n\n### Cross-layer communication\n\n| Mechanism                 | Example                             | Purpose                                           |\n| ------------------------- | ----------------------------------- | ------------------------------------------------- |\n| `@dataclass(frozen=True)` | `ModelConfig`, `SynthesisResult`    | Immutable snapshots passed between layers         |\n| `@dataclass` (mutable)    | `SynthesisConfig`                   | Aggregates user inputs before passing down        |\n| Callbacks                 | `on_chunk(current, total, preview)` | Interface layer decides _how_ to display progress |\n\n## 🧪 Testing\n\n### Strategy\n\nEach architectural layer has its own test file with a tailored testing approach:\n\n| File                | Layer     | Tests | Mocking                            | Speed            |\n| ------------------- | --------- | ----- | ---------------------------------- | ---------------- |\n| `test_text.py`      | Domain    | 37    | None — pure functions, stdlib only | \u003c 1ms per test   |\n| `test_storage.py`   | Domain    | 9     | None — real temp files             | \u003c 1ms per test   |\n| `test_synthesis.py` | Use-Case  | 17    | `FakeModel` stubs infrastructure   | \u003c 100ms per test |\n| `test_cli.py`       | Interface | 19    | `patch_model` + `CliRunner`        | \u003c 500ms per test |\n\n### Running tests\n\n```bash\n# Quick run\npython -m pytest\n\n# With coverage report\npython -m pytest --cov=qwen_reader --cov-report=term-missing\n\n# Single layer\npython -m pytest tests/test_text.py -v\n```\n\n### Coverage\n\n| Module              | Stmts   | Miss   | Cover    |\n| ------------------- | ------- | ------ | -------- |\n| `__init__.py`       | 1       | 0      | 100%     |\n| `cli/app.py`        | 22      | 2      | 91%      |\n| `cli/commands.py`   | 103     | 8      | 92%      |\n| `cli/options.py`    | 12      | 0      | **100%** |\n| `cli/rendering.py`  | 22      | 0      | **100%** |\n| `core/text.py`      | 52      | 0      | **100%** |\n| `core/storage.py`   | 35      | 0      | **100%** |\n| `core/synthesis.py` | 74      | 3      | 96%      |\n| `core/model.py`     | 33      | 15     | 55%      |\n| **Total**           | **358** | **30** | **92%**  |\n\n\u003e [!NOTE]\n\u003e `core/model.py` coverage is lower by design — it wraps `torch` and `qwen_tts` which are mocked in tests. The remaining uncovered lines are the actual model loading path that requires a GPU.\n\n### Coverage targets\n\n| Layer     | Minimum | Target | Actual  |\n| --------- | ------- | ------ | ------- |\n| Domain    | 90%     | 100%   | ✅ 100% |\n| Use-Case  | 80%     | 90%    | ✅ 96%  |\n| Interface | 60%     | 80%    | ✅ 92%  |\n\n## 🔒 Error handling \u0026 exit codes\n\n### Error taxonomy\n\n| Category           | Exception           | CLI behavior                  |\n| ------------------ | ------------------- | ----------------------------- |\n| File not found     | `FileNotFoundError` | Print message, continue batch |\n| Unsupported format | `ValueError`        | Print message, continue batch |\n| Empty content      | `ValueError`        | Print message, continue batch |\n| Model failure      | `RuntimeError`      | Print message, exit 1         |\n| Synthesis failure  | `RuntimeError`      | Print message, exit 1         |\n\n### Exit codes\n\n| Code | Meaning                                   |\n| ---- | ----------------------------------------- |\n| `0`  | All operations succeeded                  |\n| `1`  | One or more operations failed             |\n| `2`  | CLI usage error (missing args, bad flags) |\n\nCore modules never call `sys.exit()` — they raise typed exceptions. Only `cli/commands.py` converts exceptions to exit codes.\n\n## ⚙️ Configuration\n\nConfiguration follows a strict priority order: **CLI flags → Environment variables → Dataclass defaults**.\n\n| Variable              | Default                                | Description              |\n| --------------------- | -------------------------------------- | ------------------------ |\n| `QWEN_TTS_MODEL`      | `Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice` | HuggingFace model ID     |\n| `QWEN_TTS_DEVICE`     | _auto_ (`cuda:0` if available, else `cpu`) | Inference device         |\n| `QWEN_TTS_OUTPUT_DIR` | `~/qwen-reader-audio`                  | Default output directory |\n\nEnvironment variables are read inside `default_factory` on config dataclasses — never scattered through application logic.\n\n## 📋 Requirements\n\n| Dependency                                       | Purpose                   |\n| ------------------------------------------------ | ------------------------- |\n| [qwen-tts](https://pypi.org/project/qwen-tts/)   | Qwen3-TTS model inference |\n| [torch](https://pytorch.org/)                    | Deep learning runtime     |\n| [soundfile](https://pypi.org/project/soundfile/) | WAV file I/O              |\n| [numpy](https://numpy.org/)                      | Audio array operations    |\n| [click](https://click.palletsprojects.com/)      | CLI framework             |\n| [rich](https://rich.readthedocs.io/)             | Terminal formatting       |\n| [flash-attn](https://github.com/Dao-AILab/flash-attention) _(optional, Linux)_ | ~2× faster inference, less VRAM |\n\n**Dev dependencies** (optional):\n\n| Dependency                                       | Purpose            |\n| ------------------------------------------------ | ------------------ |\n| [pytest](https://docs.pytest.org/)               | Test framework     |\n| [pytest-cov](https://pytest-cov.readthedocs.io/) | Coverage reporting |\n\n## ✅ Readiness checklist\n\nEvery item must pass before merge to `main`.\n\n### Architecture (A1–A5)\n\n- [x] Interface layer (`cli/`) imports no infrastructure/domain heavy deps\n- [x] Domain layer (`core/text.py`, `core/storage.py`) has zero third-party imports\n- [x] Core modules never call `print()`, `sys.exit()`, or import `click`/`rich`\n- [x] All cross-layer data flows via `@dataclass` or callbacks\n- [x] Heavy imports (torch, model libs) are deferred inside functions\n\n### Packaging (P1–P4)\n\n- [x] `pyproject.toml` has `[project.scripts]` entry\n- [x] `__main__.py` exists and delegates to `cli:main`\n- [x] `__init__.py` exports only `__version__`\n- [x] `pip install -e .` + `qwen-reader --help` succeeds\n\n### Developer experience (D1–D6)\n\n- [x] `-h`/`--help` available on every group and command\n- [x] `-v`/`--version` prints version and exits\n- [x] All options have `show_default=True` where applicable\n- [x] Success output is a structured Rich panel/table\n- [x] Error output uses `[red]❌` prefix\n- [x] Exit codes follow contract (0/1/2)\n\n### Robustness (R1–R5)\n\n- [x] Empty file / empty text raises `ValueError`, not crash\n- [x] Unsupported extension raises `ValueError` with list of valid types\n- [x] File encoding fallback (UTF-8 → Latin-1) is implemented\n- [x] Windows UTF-8 stdout reconfiguration is present\n- [x] Batch processing continues on per-file errors\n\n### Code quality (Q1–Q5)\n\n- [x] Every public function has a docstring with Args/Returns/Raises\n- [x] Module-level docstring states purpose and dependency contract\n- [x] Type annotations on all public function signatures\n- [x] No `# type: ignore` without adjacent comment explaining why\n- [x] Constants use `UPPER_SNAKE_CASE`, classes `PascalCase`\n\n### Testing (T1–T3)\n\n- [x] Domain layer has unit tests with no mocks (46 tests)\n- [x] Use-Case layer has tests that mock infrastructure (17 tests)\n- [x] CLI layer has click `CliRunner` tests (19 tests)\n\n## 📄 License\n\nThis project is licensed under the [MIT License](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fluis-codex%2Fqwen-reader","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fluis-codex%2Fqwen-reader","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fluis-codex%2Fqwen-reader/lists"}