Lulucat

Kimi K3 কীভাবে নিজে একটি ডিজিটাল ফাউন্টেন পেনের স্ট্রোক তৈরি করল

Gaoge ZhangGaoge Zhang

Lulucat Notes-এ আমরা Freeform-এর দিকনির্দেশনামূলক ফাউন্টেন পেনটি চেয়েছিলাম। স্পেসিফিকেশন ছিল হাতের লেখার একটি স্ক্রিনশট। দুটি মডেল, একটি পার্থক্য নির্ণায়ক প্রশ্ন এবং একটি উপবৃত্তাকার নিবের পর জলকলমটি এখন ঠিক যেভাবে লেখা উচিত সেভাবেই লেখে। Fireworks-এ Kimi K3 গবেষণা, কোড ও যাচাইকরণ করেছে।

হালনাগাদ

হাতে লেখা ইংরেজি কার্সিভে "fountain pen" লেখা আছে চীনা ক্যালিগ্রাফির একটি লাইনের ওপরে; ক্রিম রঙের কাগজে উপবৃত্তাকার-নিবের ফাউন্টেন পেন দিয়ে আঁকা: উল্লম্ব দাগ মোটা, অনুভূমিক দাগ সরু।

এই পোস্টে আলোচিত Lulucat Notes-এর Metal pipeline-ই এটি রেন্ডার করেছে।

Lulucat Notes-এ একটি সাধারণ কলম ও একটি হাইলাইটার আছে। রোডম্যাপের পরের টুল ছিল একটি জলকলম — Apple Notes ও Freeform-এ পরিচিত সেই দিকনির্দেশনামূলক ফাউন্টেন পেন, যেখানে উল্লম্ব দাগ মোটা এবং অনুভূমিক দাগ সরু হয়। এই টুলের স্পেসিফিকেশন কোনো নথি ছিল না; ছিল একটি স্ক্রিনশট: হাতের লেখার তিনটি লাইন, শব্দগুলি “pencilkit”, “无边记” এবং “这种有方向的水笔能力” — অর্থাৎ এই দিকনির্দেশনামূলক জলকলমের ক্ষমতা।

আগের Metal pipeline-এর মতো, এই কাজটিও করেছে Kimi K3—Fireworks-এ চলা Moonshot-এর উন্মুক্ত মডেল: পরিমাপ, মডেলিং, কোড ও যাচাইকরণ। মানুষটি স্ক্রিনশট দিয়েছে, একটি প্রশ্নের উত্তর দিয়েছে এবং আসল iPad-এ লেখার অনুভূতি বিচার করেছে।

স্পেসিফিকেশন একটি স্ক্রিনশট

একটি স্থির ছবি আপনাকে বলতে পারে না কোনো দাগ কেন সরু। সে শুধু বলতে পারে দাগটি কতটা সরু এবং কোথায়। তাই প্রথম ধাপ ছিল পরিমাপ। আমরা স্ক্রিনশটটি সারি ধরে এবং কলাম ধরে স্ক্যান করেছি; প্রতিটি দাগের কেন্দ্ররেখার সরে যাওয়া অনুসরণ করে তার দিক নির্ণয় করেছি, তারপর সেই কোণের সাইন ব্যবহার করে স্ক্যানের প্রস্থকে প্রকৃত প্রস্থে রূপান্তর করেছি:

দাগদিকপ্রকৃত প্রস্থ
“pencilkit”-এর “l” অক্ষরের ঊর্ধ্বমুখী দাগ≈ 78°27.3 px
“pencilkit”-এর “k” অক্ষরের দণ্ড≈ 90°28 px
力-এর বাঁদিকে নামা বাঁক≈ 66°25.7 px
কার্সিভের সংযোগকারী দাগ≈ 8°11 px
চীনা অনুভূমিক দাগ (横)≈ 0°6–11 px

মোটা ও সরুর অনুপাত ≈ 2.5। 66° বিন্দুর সঙ্গে মেলালে পাওয়া যায় — -এর ক্ষেত্রে প্রায় রৈখিক, এবং উল্লম্ব অক্ষে সর্বোচ্চ। বাস্তবে এটি একটি ইটালিক নিব: একটি অনুভূমিক চেরা, যা অনুভূমিক লেখায় কখনও পুরোপুরি খোলে না।

প্রথম সংস্করণ: দিক থেকে প্রস্থ

প্রথম মডেলটি ছিল সবচেয়ে স্বাভাবিক অনুমান। প্রতিটি ইনপুট বিন্দুর জন্য দিক নির্ণয় করা, নির্ধারিত বক্ররেখার মাধ্যমে দিককে প্রস্থে রূপান্তর করা এবং ক্যাপচারের সময় ফলটিকে বিন্দুর ব্যাসার্ধে স্থায়ীভাবে বসিয়ে দেওয়া। দিকটি causal ভাবে নির্ণয় করা হতো — বক্ররেখার শেষ কয়েকটি বিন্দুর ওপর এক্সপোনেনশিয়ালি কমতে থাকা ওজনযুক্ত গড় হিসেবে, দ্বিগুণ-কোণ স্থানে হিসাব করে, যাতে চলার দিক উল্টে গেলে নির্ণয়টি বাতিল না হয়। শুধু আগের বিন্দুগুলো ব্যবহার করা হয়, তাই লাইভ দাগ এবং চূড়ান্ত দাগ বাইটে বাইটে মিলে যায়।

বিল্ডারটির ইউনিট পরীক্ষা পাস করেছে: কৃত্রিম অনুভূমিক, উল্লম্ব, 45° এবং বিপরীতমুখী পথগুলো সবই তাত্ত্বিক প্রস্থ বসিয়েছে — 0.99, 2.52, 2.00 এবং 0.99 point। রেন্ডারিংয়ে একটিও নতুন কোড লাগেনি: ব্যাসার্ধ-স্থির করা দাগ আসলে গোল ছাপের একটি শৃঙ্খল, আর আমাদের point-sprite pipeline সেগুলো আগেই আঁকত।

iPad-এ মানুষটি প্রায় দশ সেকেন্ডেই এটি বাতিল করল: “এটি একটি আর্ট পেন, জলকলম নয়।”

একটি ছবিতে দুটি ব্যাখ্যা

এটি ভুল মনে হচ্ছিল কেন? দুটি সম্ভাব্য ব্যাখ্যা ছিল, আর স্ক্রিনশটটি তাদের আলাদা করতে পারছিল না:

  • দিক-লক। নিবের জ্যামিতি হাত যা-ই করুক, অনুভূমিক দাগ সরু এবং উল্লম্ব দাগ মোটা রাখে। প্রথম সংস্করণে এটাই বাস্তবায়ন করা হয়েছিল।
  • চাপ ও গতি। কলমটি চাপ-নির্ভর, আর নমুনার ধরনটি কেবল হাতের লেখার গতিশীলতা: নিচের দিকে টানা দাগে স্বাভাবিকভাবেই বেশি চাপ পড়ে, সংযোগকারী দাগগুলো স্বাভাবিকভাবেই দ্রুত ও হালকা।

দুটি ব্যাখ্যাতেই সরু অনুভূমিক ও মোটা উল্লম্ব দাগসহ স্ক্রিনশট তৈরি হয়। শক্ত করে অনুভূমিক দাগে চাপ দিলে কী হয়, পার্থক্যটি সেখানে। প্রথম সংস্করণে সেটি সরুই থাকে; চাপ-নির্ভর কলমে সেটি মোটা হয়। তাই মানুষটিকে আমরা একটি প্রশ্ন করলাম: জোরে চাপ দেওয়া অনুভূমিক দাগ কি মোটা হওয়া উচিত?

“না। অনুভূমিক দাগ সরুই থাকবে।”

দিক-লক নিশ্চিত হলো। কিন্তু অন্য কিছু এখনও ভুল ছিল, কারণ প্রথম সংস্করণটিও দিক-লক করা ছিল।

উত্তরটি ছিল দাগের শেষ প্রান্তে

পরের সূত্রটি ছিল ঊর্ধ্বমুখী দাগগুলোর মাথায়। নমুনার “l” ও “k” দণ্ড বড় করে দেখে দুটি বিষয় দেখা গেল: একটি সোজা দাগ শুরু থেকে শেষ পর্যন্ত একই প্রস্থ ধরে রাখে, আর দাগের শেষগুলি সমতল তির্যক কাট — চিজেল নিব তুলে নেওয়ার সময় যেমন আকৃতি হয়। গোল বিন্দু নয়। চাপের কারণে সরু হয়ে যাওয়াও নয়।

নমুনা থেকে হাতে লেখা দুটি ঊর্ধ্বমুখী দণ্ডের ক্লোজ-আপ; প্রতিটির মাথা একটি সমতল তির্যক কাটে শেষ হয়েছে এবং দণ্ডজুড়ে পুরুত্ব সমান।

“আর্ট পেনের অনুভূতি” বলতে আসলে এটাই বোঝানো হয়েছিল। প্রথম সংস্করণ নমুনার চেহারা মডেল করেছিল — অনুমিত দিকের ফাংশন হিসেবে প্রস্থ — কিন্তু কলমটিকে মডেল করেনি। দিক-নির্ণায়ক একটি সেন্সর: গোলমেলে ইনপুটে কেঁপে ওঠে, কোণের কাছে দেরি করে এবং প্রতিটি দাগের শেষকে একটি বৃত্তে পরিণত করে। আসল নিবের এসব সমস্যা নেই, কারণ সেটি কিছু গণনা করে না। প্রস্থই জ্যামিতি।

দ্বিতীয় সংস্করণ: একটি উপবৃত্তাকার নিব

চূড়ান্ত মডেলে কোনো দিক-নির্ণায়ক নেই। নিবটি একটি অভিমুখী উপবৃত্ত: দীর্ঘ অক্ষ অনুভূমিক, ক্ষুদ্র অক্ষ স্থির। এই উপবৃত্তের ছাপগুলো দাগের পথ বরাবর ঘন করে বসানো হয়, আর বাকি সবই জ্যামিতি থেকে বেরিয়ে আসে:

  • অনুভূমিক দাগ খোলা প্রান্ত বরাবর চলে, তাই যেকোনো চাপে তার প্রস্থ সবসময় — একটি স্থির সরু রেখা। নিশ্চিত হওয়া স্পেসিফিকেশনের সঙ্গে হুবহু মেলে।

  • উল্লম্ব দাগ পুরো দীর্ঘ অক্ষ অতিক্রম করে: , অর্থাৎ মোটা প্রান্ত।

  • তির্যক দাগ চলার দিকের লম্বে উপবৃত্তের জ্যা-প্রস্থ নেয়,

  • দাগের শেষগুলি উপবৃত্তাকার কাট — নমুনার সমতল নিব-আকৃতির প্রান্ত, কোনো অতিরিক্ত কাজ ছাড়াই।

  • চাপ শুধু দীর্ঘ অক্ষের স্কেল বদলায়, , ফলে নিচের দিকে টানা দাগে চাপ কালির পরিমাণ বাড়ায়, কিন্তু অনুভূমিক দাগকে কখনও মোটা করতে পারে না।

নমুনার মাপ থেকে আমরা pt এবং pt রেখেছি, একই 2.5 অনুপাত। ছাপগুলো বক্ররেখা বরাবর 0.5 point দূরে বসে; 0.55-point ক্ষুদ্র ব্যাসার্ধে সবচেয়ে খারাপ ক্ষেত্রেও প্রান্তের ঢেউ প্রায় এক দশমাংশ pixel।

রেন্ডারিংয়ের জন্য একটি নতুন fragment shader ছাড়া আর কিছু লাগেনি। Vertex format — position, diameter, color — আগেই সব বহন করত: diameter দীর্ঘ অক্ষ, আর ক্ষুদ্র অক্ষ প্রতি-পাস uniform। Shader আমাদের গোল ছাপের মতো একই half-pixel coverage ramp ব্যবহার করে ellipse SDF নির্ণয় করে; ফলে এটি Core Graphics-এর reference implementation (fillEllipse per stamp)-এর সঙ্গে pixel-level-এ তুলনীয় থাকে। যাচাইকরণ স্বাভাবিক ধাপ পেরিয়েছে: দুটি ফাউন্টেন দাগসহ একটি কৃত্রিম করপাস, দুইভাবে রেন্ডার করে পিক্সেল-বাই-পিক্সেল তুলনা — শূন্য কাঠামোগত পার্থক্য; নমুনা হিসেবে যাচাই করা একটি অনুভূমিক দাগ দুই রেন্ডারারেই 12 px মাপা হয়েছে।

একই হাতে লেখা "fountain" শব্দের দুটি রেন্ডারিং পাশাপাশি: বাঁদিকে দিক-স্কেল করা ব্যাসের গোল ছাপ, যেখানে গোল প্রান্ত দেখা যায়; ডানদিকে উপবৃত্তের ছাপ, যেখানে সমতল নিব-কাটা প্রান্ত ও আরও স্থির প্রস্থ দেখা যায়।

প্রথম সংস্করণ বাঁদিকে, দ্বিতীয়টি ডানদিকে। একই হাতে লেখা ইনপুট, একই pipeline। প্রান্তই সব বলে দেয়।

iPad-এ নতুন কলমটি সঙ্গে সঙ্গেই পাস করল: “好,很好” — ভালো। খুব ভালো।

ব্যর্থ প্রচেষ্টাটি রেখে দেওয়া

প্রথম সংস্করণটি আবর্জনায় যায়নি। এটি আকর্ষণীয় একটি ব্রাশ — শুধু জলকলম নয়। তাই এটি নতুন পরীক্ষামূলক-ব্রাশ মেনুর প্রথম এন্ট্রি হিসেবে প্রকাশিত হলো, মানুষটির দেওয়া নামে: 漏水的圆珠笔, অর্থাৎ লিক করা বলপেন। টুলবারে একটি ফ্লাস্ক বোতাম যোগ হলো, যা পরীক্ষাগুলোর একটি পাঠ্যতালিকা খুলে দেয়; পরেরটি যোগ করতে রেজিস্ট্রিতে মাত্র এক লাইনই যথেষ্ট। কোনো প্রত্যাখ্যাত মডেলকে স্পষ্টভাবে চিহ্নিত পরীক্ষা হিসেবে প্রকাশ করা গেলে সেটি নষ্ট পরিশ্রম নয়।

এক সন্ধ্যার চক্র

পুরো পরিক্রমা — পরিমাপ, মডেল, নির্মাণ, ডিভাইসে পরীক্ষা, প্রশ্ন, পুনর্মডেলিং, পুনর্নির্মাণ, যাচাইকরণ — এক সন্ধ্যায় শেষ হয়েছিল। Fireworks-এ Kimi K3 শুধু কোড লেখেনি; চক্রটির পুরো প্রযুক্তিগত দিক চালিয়েছে: পরিমাপের স্ক্যানের পদ্ধতি তৈরি করেছে, দ্বিতীয়বার অনুমান না করে পার্থক্য নির্ণায়ক প্রশ্ন প্রস্তাব করেছে, প্রমাণ বদলে গেলে নিজের দিক-নির্ণায়ক মুছে ফেলেছে এবং অ্যাপে হাত দেওয়ার আগেই pixel-diff harness বাড়িয়েছে। Fireworks-এর ইনফারেন্সের গতি এই চক্রকে ইন্টার‌্যাক্টিভ রেখেছিল — দীর্ঘ Metal ও Swift diff, পিক্সেল-অ্যানালাইসিস স্ক্রিপ্ট এবং corpus tooling এত দ্রুত এসেছিল যে bottleneck যেখানে থাকা উচিত ছিল সেখানেই ছিল: মানুষের বিচারবোধ।

Metal pipeline পোস্টে আমরা যে সহযোগিতার ধরন নিয়ে লিখেছিলাম, সেটিই আবার কাজ করেছে: মডেল দ্রুত ও নির্ভুল; মানুষের হাতে রুচি ও শুরু-থেকে-শেষ যাচাইকরণ। অনুভূতিনির্ভর প্রতিক্রিয়ার একটি বাক্য — “আর্ট পেন, জলকলম নয়” — মডেলকে সঠিক মডেলিং-ত্রুটি খুঁজে নিয়ে আরও সরল নকশা দিয়ে বদলে দিতে যথেষ্ট ছিল।

সঠিক মডেলটি ভুলটির চেয়ে ছোট হয়ে দাঁড়াল। দ্বিতীয় সংস্করণটি প্রথমটির চেয়ে কম চলমান অংশ নিয়ে প্রকাশিত হলো — কোনো দিক-নির্ণায়ক নেই, মসৃণকরণ উইন্ডো নেই, ফিট করা সূচক নেই। নিবটি প্রস্থ গণনা করে না। নিবই প্রস্থ